本节主要依据OpenConfig初识(二)在linux环境下进行gNMI客户端和服务端的实际部署,以进一步了解gNMI的工作原理和使用方法。
本节go语言采用go1.15.2 linux/amd64版本,在$GOPATH/src目录下,创建gnmi-test文件夹,并在gnmi-test文件夹下创建client_app和gnxi_target子文件夹,分别用来存放gNMI客户端和服务端相关文件,为了方便环境部署,go环境变量设置如下:
export GO111MODULE=on
export GOPROXY=https://goproxy.cn
创建gNMI服务端
gNxI是一种使用gNMI(gRPC网络管理接口)和gNOI(gRPC网络操作接口)协议的网络管理工具集。此处,使用该工具集中的gnmi_target作为gNMI服务端,创建gNMI服务前,先简单了解一下gnxi工具集。
gnxi工具集
通过下面的命令可以编译并安装gnxi相关工具:
cd $GOPATH/bin
go install github.com/google/gnxi/...
gnxi/certs目录下的generate.sh脚本可用于生成客户端和服务端之间的认证证书,运行该脚本,结果如下:
[root@localhost certs]# ./generate.sh
[root@localhost certs]# ls
ca.crt ca.key client.crt client.key README.md target.csr
ca.csr ca.srl client.csr generate.sh target.crt target.key
其中,ca.crt是CA证书授权;ca.key是CA私有秘钥;client.crt/target.crt是由CA授权的client/target证书;client.key/target.key是相应client/target的私有秘钥。
将ca.crt, target.crt, target.key和gnxi/gnmi_target/openconfig-openflow.json文件放到$GOPATH/src/gnmi-test/gnxi_target/文件夹下,同样将ca.crt, client.crt, client.key拷贝到$GOPATH/src/gnmi-test/client_app目录下,分别在两个目录下,开启两个终端,执行如下命令:
[root@localhost gnxi_target]# gnmi_target -bind_address :9339 -config openconfig-openflow.json \
-key target.key -cert target.crt -ca ca.crt -alsologtostderr
I1014 08:58:23.265109 40368 gnmi_target.go:119] starting to listen on :9339
I1014 08:58:23.346864 40368 gnmi_target.go:125] starting to serve
[root@localhost client_app]# gnmi_get -xpath "/system/openflow/agent/config/datapath-id" \
-xpath "/system/openflow/controllers/controller[name=main]/connections/connection[aux-id=0]/state/address" \
-target_addr localhost:9339 -target_name target.com \
-key client.key -cert client.crt -ca ca.crt -alsologtostderr
== getRequest:
path: <
elem: <
name: "system"
>
elem: <
name: "openflow"
>
elem: <
name: "agent"
>
elem: <
name: "config"
>
elem: <
name: "datapath-id"
>
>
path: <
elem: <
name: "system"
>
elem: <
name: "openflow"
>
elem: <
name: "controllers"
>
elem: <
name: "controller"
key: <
key: "name"
value: "main"
>
>
elem: <
name: "connections"
>
elem: <
name: "connection"
key: <
key: "aux-id"
value: "0"
>
>
elem: <
name: "state"
>
elem: <
name: "address"
>
>
encoding: JSON_IETF
== getResponse:
notification: <
timestamp: 1602637327544603349
update: <
path: <
elem: <
name: "system"
>
elem: <
name: "openflow"
>
elem: <
name: "agent"
>
elem: <
name: "config"
>
elem: <
name: "datapath-id"
>
>
val: <
string_val: "00:16:3e:00:00:00:00:00"
>
>
>
notification: <
timestamp: 1602637327550461350
update: <
path: <
elem: <
name: "system"
>
elem: <
name: "openflow"
>
elem: <
name: "controllers"
>
elem: <
name: "controller"
key: <
key: "name"
value: "main"
>
>
elem: <
name: "connections"
>
elem: <
name: "connection"
key: <
key: "aux-id"
value: "0"
>
>
elem: <
name: "state"
>
elem: <
name: "address"
>
>
val: <
string_val: "192.0.2.10"
>
>
>
可以看到客户端通过gnmi_get可以获取到服务端openconfig-openflow.json配置文件中的配置信息。同样的,gnmi_capabilities可以获取到服务端支持的能力集;gnmi_set可以设置服务端的配置信息;gnmi_subscribe可以订阅服务端的信息,当然还有其他工具,详细请参考gNxI。
基于openconfig-interface.yang创建gnmi服务
openconfig-interface.yang在OpenConfig初识(二)中已经给出,下面根据openconfig-interface.yang创建gnmi服务,是的启动该服务后,服务会加载由openconfig-interface.yang绑定的go数据结构,然后客户端和服务端就可以根据该YANG模型定义的数据结构进行信息交互了。
gnxi/gnmi/modeldata/gostruct目录下有两个go文件:gen.go和generated.go。gen.go给出了利用YANG文件生成generated.go的方法,已经提供的generated.go文件头注释部分如下:
/*
Package gostruct is a generated package which contains definitions
of structs which represent a YANG schema. The generated schema can be
compressed by a series of transformations (compression was false
in this case).
This package was generated by go/src/github.com/openconfig/ygot/genutil/names.go
using the following YANG input files:
- github.com/openconfig/public/release/models/interfaces/openconfig-interfaces.yang
- github.com/openconfig/public/release/models/openflow/openconfig-openflow.yang
- github.com/openconfig/public/release/models/platform/openconfig-platform.yang
- github.com/openconfig/public/release/models/system/openconfig-system.yang
Imported modules were sourced from:
- github.com/openconfig/public/...
- github.com/YangModels/yang/...
*/
从文件头可以看出,该文件由github.com/openconfig/ygot/genutil/names.go生成,使用的yang文件包括:openconfig-interfaces.yang,openconfig-openflow.yang,openconfig-platform.yang,openconfig-system.yang等,基于此,我们利用自己写的openconfig-interface.yang文件生成generated.go文件。
首先将openconfig-interface.yang文件及其依赖openconfig-extensions.yang文件放入$GOPATH/src/gnmi-test/gnxi_target/目录下,执行如下命令:
[root@localhost gostruct]# go run /root/go/pkg/mod/github.com/openconfig/ygot@v0.8.0/generator/generator.go \
-generate_fakeroot -output_file /root/go/src/gnxi-master/gnmi/modeldata/gostruct/generated.go \
-package_name gostruct /root/go/src/gnmi-test/gnxi_target/openconfig-interface.yang
生成的generated.go文件头部注释变为:
/*
Package gostruct is a generated package which contains definitions
of structs which represent a YANG schema. The generated schema can be
compressed by a series of transformations (compression was false
in this case).
This package was generated by /root/go/pkg/mod/github.com/openconfig/ygot@v0.8.0/genutil/names.go
using the following YANG input files:
- /root/go/src/gnmi-test/gnxi_target/openconfig-interface.yang
Imported modules were sourced from:
*/
将OpenConfig初识(二)节中的interface.json文件拷贝到$GOPATH/src/gnmi-test/gnxi_target/目录下,执行如下命令:
go run /root/go/src/gnxi-master/gnmi_target/gnmi_target.go \
-bind_address :10161 -config interface.json -key target.key \
-cert target.crt -ca ca.crt -username foo -password bar -alsologtostderr
如果出现类似错误内容:…/…/gnxi-master/gnmi_target/gnmi_target.go:27:2: cannot find module providing package github.com/golang/glog: working directory is not part of a module
当前目录下执行:
[root@localhost gnxi_target]# go mod init
出现类似错误内容:
F1014 10:00:46.662203 45195 gnmi_target.go:114] error in creating gnmi target: parent container interfaces (type *gostruct.OpenconfigInterfaces_Interfaces): JSON contains unexpected field config
exit status 1
错误原因可能是生成generated.go文件的路径不对,程序找不到生成的generated.go,为此,将文件输出到/root/go/pkg/mod/github.com/google/gnxi@v0.0.0-20201002114532-88db804e4a55/gnmi/modeldata/gostruct/目录下,执行如下命令:
[root@localhost gnxi_target]# go run /root/go/pkg/mod/github.com/openconfig/ygot@v0.8.0/generator/generator.go \
-generate_fakeroot -output_file /root/go/pkg/mod/github.com/google/gnxi@v0.0.0-20201002114532-88db804e4a55/gnmi/modeldata/gostruct/generated.go \
-package_name gostruct /root/go/src/gnmi-test/gnxi_target/openconfig-interface.yang
出现如下错误:
../../gnxi-master/gnmi_target/gnmi_target.go:87:3: undefined: gostruct.ΛEnum
将文件gnmi_target.go的87行参数修改为nil, 继续执行,输出结果如下:
[root@localhost gnxi_target]# go run /root/go/src/gnxi-master/gnmi_target/gnmi_target.go -bind_address :10161 \
-config interface.json -key target.key -cert target.crt -ca ca.crt \
-username foo -password bar -alsologtostderr
I1014 10:45:36.842286 46996 gnmi_target.go:119] starting to listen on :10161
I1014 10:45:36.872633 46996 gnmi_target.go:125] starting to serve
如此,gNMI服务成功启动了。
创建gNMI客户端
在$GOPATH/src/gnmi-test/client_app目录下,执行如下命令:
[root@localhost client_app]# python -m grpc.tools.protoc --proto_path=/root/go/src/gnmi-master/proto/gnmi_ext/ \
--proto_path=/root/go/src/gnmi-master/proto/gnmi/ --python_out=./ --grpc_python_out=./ \
/root/go/src/gnmi-master/proto/gnmi_ext/gnmi_ext.proto
[root@localhost client_app]# python -m grpc.tools.protoc --proto_path=/root/go/src/gnmi-master/proto/gnmi_ext/ \
--proto_path=/root/go/src/gnmi-master/proto/gnmi/ --python_out=./ --grpc_python_out=./ \
/root/go/src/gnmi-master/proto/gnmi_ext/gnmi_ext.proto
生成gnmi_pb2.py & gnmi_pb2_grpc.py, gnmi_ext_pb2.py & gnmi_ext_pb2_grpc.py文件,将OpenConfig初识(二)节中的simple_client.py脚本稍作修改,内容如下:
import sys
sys.path.append('/root/go/src/gnmi-test/client_app') # For importing gnmi protos
import gnmi_pb2
import gnmi_pb2_grpc
import grpc
import json
def SetUpChannel():
"""Set up the gNMI RPC."""
wdir = '/root/go/src/gnmi-test/client_app/'
# Specify the credentials in use.
creds = grpc.ssl_channel_credentials(root_certificates=open(wdir +
'ca.crt').read(), private_key=open(wdir +
'client.key').read(), certificate_chain=open(wdir +
'client.crt').read())
# Assgin the gRPC channel to our gNMI Target.
channel = grpc.secure_channel('target.com:10161', creds) """ www.example.com修改target.com"""
stub = gnmi_pb2_grpc.gNMIStub(channel)
return stub
def GetRequest(stub, path):
"""Issue a gNMI GetRequest for the given path."""
path_list = [gnmi_pb2.PathElem(name=path, key={})]
paths = gnmi_pb2.Path(elem=path_list)
# Metadata is User/pass for our example gNMI Target
response = stub.Get(gnmi_pb2.GetRequest(path=[paths]), metadata=[
('username', 'foo'), ('password', 'bar')])
return response
if __name__ == '__main__':
stub = SetUpChannel()
gnmi_response = GetRequest(stub, 'interfaces')
print(gnmi_response) #Print as PB
执行该脚本文件:
[root@localhost client_app]# python simple_client.py
结果有如下错误输出:
E1014 11:04:29.212096072 48266 ev_epollex_linux.cc:516] Error shutting down fd 9. errno: 9
Traceback (most recent call last):
File "simple_client.py", line 32, in <module>
gnmi_response = GetRequest(stub, 'interfaces')
File "simple_client.py", line 27, in GetRequest
('username', 'foo'), ('password', 'bar')])
File "/usr/lib64/python2.7/site-packages/grpc/_channel.py", line 826, in __call__
return _end_unary_response_blocking(state, call, False, None)
File "/usr/lib64/python2.7/site-packages/grpc/_channel.py", line 729, in _end_unary_response_blocking
raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "DNS resolution failed for service: target.com:10161"
debug_error_string = "{"created":"@1602644669.212669048","description":"Resolver transient failure","file":"src/core/ext/filters/client_channel/resolving_lb_policy.cc","file_line":215,"referenced_errors":[{"created":"@1602644669.212663340","description":"DNS resolution failed for service: target.com:10161","file":"src/core/ext/filters/client_channel/resolver/dns/c_ares/dns_resolver_ares.cc","file_line":378,"grpc_status":14,"referenced_errors":[{"created":"@1602644669.212057152","description":"C-ares status is not ARES_SUCCESS qtype=AAAA name=target.com is_balancer=0: Could not contact DNS servers","file":"src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.cc","file_line":287,"referenced_errors":[{"created":"@1602644667.213547029","description":"C-ares status is not ARES_SUCCESS qtype=A name=target.com is_balancer=0: Could not contact DNS servers","file":"src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.cc","file_line":287}]}]}]}"
这是由于客户端和服务端进行TCP连接验证时找不到target.com,编辑/etc/hosts文件,文件末尾添加:
172.20.10.11 target.com
在此运行simple_client.py脚本,结果如下:
[root@localhost client_app]# python simple_client.py
notification {
timestamp: 1602645066579990669
update {
path {
elem {
name: "interfaces"
}
}
val {
json_val: "{\"config\":{\"enabled\":true},\"state\":{\"enabled\":true,\"rx-packets\":73}}"
}
}
}
如此客户端成功连接服务端,并且客户端可通过编程实现对服务端的配置和管理操作。
完毕!