1、TeleNOS命令行框架简介
TeleNOS系统按照SONIC社区的架构,应用Dell命令框架,实现了sonic-mgmt-framework和sonic-mgmt-common,分别对应用户端Client和服务端Server,命令行框架运行在docker-sonic-mgmt-framework中,用户端通过前端下发查询信息(OC-Path),服务端通过yang模型查找指定的oc-yang路径,读写数据库(Redis)并经过相应的后端处理函数把对应的节点信息处理后经模版(jinja2)渲染后输出显示给用户。关于Dell框架的详细介绍请参见:SONiC管理框架介绍。
2、命令行需求描述
在设备中缺少对SSH会话的最大连接数、最大连接时间的显示,以及最大连接时间的手动配置更改,对SSH会话最大连接数配置配置为128固定并显示,用户可配置SSH最大连接时间,范围为0-12h(0为不断连)。
3、添加前端命令
在命令行框架中命令的写入是在mgmt-framework中的cli-xml中,通过修改添加xml文件添加各个命令行形式。在现有的ssh.xml中添加如下命令:分别为show ssh-session显示和ssh-session timeout、no ssh-session配置
<COMMAND name="show ssh-session" help="Display SSH session information">
<ACTION builtin="clish_pyobj">sonic_cli_ssh_vrf get_openconfig_system_ssh_session </ACTION>
<!-- ssh timeout -->
<COMMAND name="ssh-session"
help="Configure SSH session"/>
<COMMAND
name="ssh-session timeout"
help="Set SSH timeout value"
mode="subcommand"
ptype="SUBCOMMAND"
command_tables="sonic-ssh-server:sonic-ssh-server/SSH_SERVER/SSH_SERVER_LIST/ssh_name"
>
<PARAM
name="time"
help="timeout value in seconds (Max: 12 * 3600s, Min 0 without setting timeout)"
ptype="SSH_SERVER_TIMEOUT_RANGE"
dbpath="sonic-ssh-server:sonic-ssh-server/SSH_SERVER/SSH_SERVER_LIST/time_out"
>
</PARAM>
<ACTION builtin="clish_pyobj">sonic_cli_ssh_vrf patch_openconfig_system_ssh_server_timeout ${time}</ACTION>
</COMMAND>
<!-- no ssh timeout -->
<COMMAND name="no ssh-session"
help="Disable SSH session"/>
<COMMAND
name="no ssh-session timeout"
help="Disable SSH session timeout"
>
<ACTION builtin="clish_pyobj">sonic_cli_ssh_vrf delete_openconfig_system_ssh_server_timeout</ACTION>
</COMMAND>
并在sonic_type.xml中新定义数据类型:即为需要的最大连接时间范围(以秒为单位)
<!--=======================================================-->
<PTYPE
name="SSH_SERVER_TIMEOUT_RANGE"
method="integer"
pattern="0..43200"
help="SSH server timeout in seconds"
/>
4、确定后端路径
在完成前端命令的定义之后前端的开发流程只剩下定义调用执行查询OC-yang信息的Python文件,以及自定义模板文件j2用以渲染显示。为了确定OC-yang路径以便调用,先对后端OC-yang路径进行定义,并编写相应的节点处理函数,用于处理数据下发数据库。
(1)定义sonic-yang:首先在现有模块sonic-mgmt-common的源码中查询有关ssh的相关定义,发现在sonic-yang中无相关定义,为了使数据库中有关于ssh的相关字段,必须手动添加关于ssh的sonic-yang字段,代码如下:
container sonic-ssh-server {
container SSH_SERVER {
description "SSH server configuration.";
list SSH_SERVER_LIST {
key "ssh_name";
leaf ssh_name {
type string;
default "GLOBAL";
description
"It's value is always GLOBAL";
}
leaf enable {
type boolean;
default true;
description
"Enables the ssh server. The ssh server is enabled by default.";
}
leaf time_out {
type uint16;
units seconds;
description
"Set the idle timeout in seconds on terminal connections to
the system for the protocol.";
}
leaf session_limit {
type uint16;
default 128;
description
"Set a limit on the number of simultaneous active terminal
sessions to the system for the protocol.";
}
}
}
}
}
在该sonic-yang文件中定义了如上几个叶子节点,节点对应的key需要有list(虽然在这里后面只有一个全局的key但也需定义list)并指定列表的key为ssh_name(后面这个name固定为GLOBAL)再上一级定义对应配置的container,完成sonic-yang的定义。
(2)定义oc-ext-yang:定义完sonic-yang之后查询到源定义中标准的oc-yang里也无相关定义,所以需要在oc-ext-yang中添加对标准oc-yang的定义(即为对oc-yang的扩展,但不可以直接修改标准oc-yang)
augment "/oc-sys:system/oc-sys:ssh-server" {
description "Additional fields for ssh server";
list ssh-server-list {
key "ssh-name";
description
"list of ssh server configuration";
leaf ssh-name {
type leafref {
path "../config/ssh-name";
}
description "SSH name";
}
container config {
description
"Configuration data for ssh server";
uses system-ext-ssh-config;
}
container state {
config false;
description
"Operational state data for ssh server";
uses system-ext-ssh-config;
}
}
}
grouping system-ext-ssh-config {
description
"Configuration data for ssh server";
leaf ssh-name {
type string;
default "GLOBAL";
description
"It's value is always GLOBAL";
}
leaf timeout {
type uint16;
units seconds;
description
"Set the idle timeout in seconds on ssh connections to
the system for the protocol.";
}
leaf session-limit {
type uint16;
default 128;
description
"Set a limit on the number of simultaneous active ssh
sessions to the system for the protocol.";
}
}
在ext中,对路径/oc-sys:system/oc-sys:ssh-server进行了扩展,可参照其他路径的形式进行添加,这里添加一个有关ssh配置和显示的list,用以保存config和state,叶子结点内容和sonic-yang中定义的一致,并定义key为ssh-name,在之后的Python调用时,路径中需要指明keyname。
(3)定义oc-annot-yang:定义完oc-ext-yang之后需要在oc-annot-yang中声明偏差(即oc-yang和sonic-yang中节点名称不相同时需要指明)和节点转换函数
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list {
deviate add {
sonic-ext:table-name "SSH_SERVER";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:config/oc-sys-ext:ssh-name {
deviate add {
sonic-ext:field-transformer "ssh_server_ssh_name";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:state/oc-sys-ext:ssh-name {
deviate add {
sonic-ext:field-transformer "ssh_server_ssh_name";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:config/oc-sys-ext:timeout {
deviate add {
sonic-ext:field-name "time_out";
sonic-ext:field-transformer "ssh_server_timeout_xfmr";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:state/oc-sys-ext:timeout {
deviate add {
sonic-ext:field-name "time_out";
sonic-ext:field-transformer "ssh_server_timeout_xfmr";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:config/oc-sys-ext:session-limit {
deviate add {
sonic-ext:field-name "session_limit";
}
}
deviation /oc-sys:system/oc-sys:ssh-server/oc-sys-ext:ssh-server-list/oc-sys-ext:state/oc-sys-ext:session-limit {
deviate add {
sonic-ext:field-name "session_limit";
}
}
如上指明了对oc-yang(oc-ext-yang)的节点对sonic-yang节点的偏差,由于field中session_limit默认为128无需数据处理,因此无需再次定义transformer,timeout需要获取下发数据,所以定义转换ssh_server_timeout_xfmr,注意:由于ssh-server-list是新定义的,需要在Redis中建立表,所以在节点最上端声明表名table-name为SSH_SERVER(对应sonic-yang中同名container),对于key为ssh-name,为了使数据库中无表时在配置时自动创建,所以不加field-name,只指定field-transformer,对于查询路径的下发见下节。
(4)实现数据转换函数transformer:如果前端下发的数据需要处理时,field需要定义转换函数,如下发的数据值均一致,则可不定义转换函数,本文以timeout的转换为例,介绍数据转换函数的实现(由于需求中请求和最终下发的timeout的值一致所以也可不定义转换,本文仅用作说明)。
func YangToDb_ssh_server_ssh_name(inParams XfmrParams) (map[string]string, error) {
return make(map[string]string), nil
}
func DbToYang_ssh_server_ssh_name(inParams XfmrParams) (map[string]interface{}, error) {
log.V(1).Infof("DbToYang_ssh_server_name: key=\"%s\"", inParams.key)
result := make(map[string]interface{})
if len((*inParams.dbDataMap)[inParams.curDb]) > 0 {
result["ssh-name"] = inParams.key
}
return result, nil
}
var YangToDb_ssh_server_timeout_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) {
res_map := make(map[string]string)
log.V(3).Info("YangToDb_ssh_server_time_xfmr: inParams.param", inParams.param)
if (inParams.param == nil) || (inParams.param.(*uint16) == nil) {
errStr := "Invalid input params " + inParams.key
log.V(3).Info("YangToDb_ssh_server_time_xfmr: ", errStr)
return res_map, tlerr.New(errStr)
}
sshTimeout := inParams.param.(*uint16)
res_map["time_out"] = strconv.Itoa(int(*sshTimeout))
log.V(3).Info("YangToDb_ssh_server_time_xfmr: sshTimeout ", sshTimeout)
return res_map, nil
}
var DbToYang_ssh_server_timeout_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) {
var err error
result := make(map[string]interface{})
var prtInst db.Value
data := (*inParams.dbDataMap)[inParams.curDb]
pTbl := data["SSH_SERVER"]
prtInst = pTbl["GLOBAL"]
ssh_server_timeout, ok := prtInst.Field["time_out"]
output, _ := strconv.ParseUint(ssh_server_timeout, 10, 16)
output16 := uint16(output)
log.Info("DbToYang_ssh_server_timeout_xfmr - ssh_server_timeout" + ssh_server_timeout)
if ok {
result["timeout"] = output16
} else {
log.Info("time_out field not found in DB")
}
return result, err
}
如上代码所示,转换函数包含两部分,一部分为DbToYang,另一部分为YangToDb,分为显示和下发。YangToDb中先从传入的inParams中获取入参inParams.param.(*uint16)(需要将该接口类型实现为需要的类型)并传入返回的res_map中(对于sonic-yang字段)实现数据下发,注意数据库中的数据类型均为string,因此需要再强转为string类型。DbToYang中返回result中的字段与oc-yang中一致,从inParams中获取数据库数据,
依次取出表名、key、field,得到对应值,再将其由string转换成oc-yang中的数据类型,返回给result的相应字段即完成数据从Redis到Yang的显示。
5、添加前端查询
完成后端部分之后需要完成在前端的调用查询Python脚本的实现,由于无法得知数据库中是否存在该表,所以在下发阶段将查询路径指定到字段的上一级即表级,按oc-yang的结构完成表名key和下发字段的指定(也可在hostcfgd中创建初始表并订阅数据库,这样就避免了初始阶段无表的情况)如下代码所示。
if func == "patch_openconfig_system_ssh_server_timeout":
keypath = cc.Path(
"/restconf/data/openconfig-system:system/ssh-server/openconfig-system-ext:ssh-server-list"
)
timeout = int(args[0])
body = {
"openconfig-system-ext:ssh-server-list": [{
"ssh-name": "GLOBAL",
"config": {
"ssh-name": "GLOBAL",
"timeout": timeout,
"session-limit": 128
}
}]
}
return api.patch(keypath, body)
elif func == "delete_openconfig_system_ssh_server_timeout":
keypath = cc.Path(
"/restconf/data/openconfig-system:system/ssh-server/openconfig-system-ext:ssh-server-list"
)
body = {
"openconfig-system-ext:ssh-server-list": [{
"ssh-name": "GLOBAL",
"config": {
"ssh-name": "GLOBAL",
"timeout": 0,
"session-limit": 128
}
}]
}
return api.patch(keypath, body)
elif func == "get_openconfig_system_ssh_session":
keypath = cc.Path(
"/restconf/data/openconfig-system:system/ssh-server/openconfig-system-ext:ssh-server-list={name}/config",
name="GLOBAL",
)
result = api.get(keypath)
show_cli_output("show_ssh_session.j2", result.content["openconfig-system-ext:config"])
return result
6、添加显示模版
在上节中调用show_cli_out_put中定义show_ssh_session.j2文件如下所示,传入字典类型json_output,参照jinja2模板语法输出显示各个字段值,非string类型需要转换。
{% if json_output -%}
{{'SshName Timeout(s) SessionLimit'}}
{{'----------- ---------- ------------'}}
{% set sshname = json_output['ssh-name'] %}
{% set timeout = json_output['timeout'] %}
{% set sessionlimit = json_output['session-limit'] %}
{{sshname.ljust(16)}}{{(timeout| string).ljust(16)}}{{sessionlimit}}
{% endif %}
7、订阅数据库并下发配置
如上步骤我们完成了命令行下发数据库并显示的过程,具体根据数据库中字段值进行配置下发则需要应用hostcfgd进程订阅相应数据库,为此需要修改hostcfgd源文件,如下代码所示:
class SshCfg(object):
"""
SshCfg Config Daemon
"""
def __init__(self):
self.ssh_timeout_default = 900
def load(self, ssh_server_conf):
syslog.syslog(syslog.LOG_INFO, "SshCfg load ...")
syslog.syslog(syslog.LOG_INFO, "ssh_server_conf:{}".format(ssh_server_conf))
cmd = "sudo cat /etc/ssh/sshd_config | grep ^ClientAliveInterval"
res = os.popen(cmd).readline()
timeout = int(res.split()[1])
if ssh_server_conf == {}:
if timeout == self.ssh_timeout_default:
return
else:
self.ssh_server_update("GLOBAL", "SET", {"time_out": self.ssh_timeout_default})
else:
timeout_db = int(ssh_server_conf["GLOBAL"]["time_out"])
if timeout_db == timeout:
return
else:
self.ssh_server_update("GLOBAL", "SET", {"time_out": timeout_db})
def ssh_server_update(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'ssh server key {}'.format(key))
timeout = data["time_out"]
if op == "SET":
cmd = "sudo sed -i '/ClientAliveInterval/c ClientAliveInterval " + str(timeout) + "' /etc/ssh/sshd_config;" + "sudo systemctl restart ssh;" + "sudo systemctl restart sshd"
run_cmd(cmd)
elif op == "DEL":
cmd = "sudo sed -i '/ClientAliveInterval/c ClientAliveInterval " + "0" + "' /etc/ssh/sshd_config;" + "sudo systemctl restart ssh;" + "sudo systemctl restart sshd"
run_cmd(cmd)
# Initialize Ssh Config Handler
self.sshcfg = SshCfg()
ssh_global = init_data['SSH_SERVER']
self.sshcfg.load(ssh_global)
def ssh_server_handler(self, key, op, data):
self.sshcfg.ssh_server_update(key, op, data)
# Handle SSH_SERVER updates
self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_server_handler))
在该文件中定义sshcfg类,并指明订阅数据库(subscribe)的表名,在初始加载过程中如无表则修改数据库创建,如果数据有变更则对ssh配置文件/etc/ssh/sshd_config的ClientAliveInterval字段下发数据(数值为0则默认关闭),实现所有步骤即完成需求命令的实现。