mirror of
https://github.com/vyos/vyos-documentation.git
synced 2025-10-26 08:41:46 +01:00
(cherry picked from commit b09f2222ed6181ba377a41de37a3997559a234c8) (cherry picked from commit 1205c1e7806d4e84e03aafc94831248cbfa9b1c8)
413 lines
11 KiB
Python
413 lines
11 KiB
Python
'''
|
|
generate json with all commands from xml for vyos documentation coverage
|
|
'''
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import re
|
|
import logging
|
|
import datetime
|
|
|
|
from io import BytesIO
|
|
from lxml import etree as ET
|
|
import shutil
|
|
|
|
default_constraint_err_msg = "Invalid value"
|
|
validator_dir = ""
|
|
|
|
|
|
input_data = [
|
|
{
|
|
"kind": "cfgcmd",
|
|
"input_dir": "_include/vyos-1x/interface-definitions/",
|
|
"schema_file": "_include/vyos-1x/schema/interface_definition.rng",
|
|
"files": []
|
|
},
|
|
{
|
|
"kind": "opcmd",
|
|
"input_dir": "_include/vyos-1x/op-mode-definitions/",
|
|
"schema_file": "_include/vyos-1x/schema/op-mode-definition.rng",
|
|
"files": []
|
|
}
|
|
]
|
|
|
|
vyos_commands_dir = "_include/coverage"
|
|
|
|
node_data = {
|
|
'cfgcmd': {},
|
|
'opcmd': {},
|
|
}
|
|
|
|
|
|
def get_vyos_commands():
|
|
return_data = None
|
|
for (dirpath, dirnames, filenames) in os.walk(vyos_commands_dir):
|
|
for file in filenames:
|
|
with open(f"{vyos_commands_dir}/{file}") as f:
|
|
data = json.load(f)
|
|
|
|
if not return_data:
|
|
return_data = data
|
|
|
|
# find latestes export
|
|
if datetime.datetime.fromisoformat(return_data['date']) < datetime.datetime.fromisoformat(data['date']):
|
|
return_data = data
|
|
|
|
return return_data
|
|
|
|
|
|
|
|
def get_properties(p):
|
|
props = {}
|
|
props['valueless'] = False
|
|
|
|
try:
|
|
if p.find("valueless") is not None:
|
|
props['valueless'] = True
|
|
except:
|
|
pass
|
|
|
|
if p is None:
|
|
return props
|
|
|
|
# Get the help string
|
|
try:
|
|
props["help"] = p.find("help").text
|
|
except:
|
|
pass
|
|
|
|
# Get value help strings
|
|
try:
|
|
vhe = p.findall("valueHelp")
|
|
vh = []
|
|
for v in vhe:
|
|
vh.append( (v.find("format").text, v.find("description").text) )
|
|
props["val_help"] = vh
|
|
except:
|
|
props["val_help"] = []
|
|
|
|
# Get the constraint statements
|
|
error_msg = default_constraint_err_msg
|
|
# Get the error message if it's there
|
|
try:
|
|
error_msg = p.find("constraintErrorMessage").text
|
|
except:
|
|
pass
|
|
|
|
|
|
vce = p.find("constraint")
|
|
vc = []
|
|
if vce is not None:
|
|
# The old backend doesn't support multiple validators in OR mode
|
|
# so we emulate it
|
|
|
|
regexes = []
|
|
regex_elements = vce.findall("regex")
|
|
if regex_elements is not None:
|
|
regexes = list(map(lambda e: e.text.strip(), regex_elements))
|
|
if "" in regexes:
|
|
print("Warning: empty regex, node will be accepting any value")
|
|
|
|
validator_elements = vce.findall("validator")
|
|
validators = []
|
|
if validator_elements is not None:
|
|
for v in validator_elements:
|
|
v_name = os.path.join(validator_dir, v.get("name"))
|
|
|
|
# XXX: lxml returns None for empty arguments
|
|
v_argument = None
|
|
try:
|
|
v_argument = v.get("argument")
|
|
except:
|
|
pass
|
|
if v_argument is None:
|
|
v_argument = ""
|
|
|
|
validators.append("{0} {1}".format(v_name, v_argument))
|
|
|
|
|
|
regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
|
|
validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))
|
|
validator_script = '${vyos_libexec_dir}/validate-value.py'
|
|
validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg)
|
|
|
|
props["constraint"] = validator_string
|
|
|
|
# Get the completion help strings
|
|
try:
|
|
che = p.findall("completionHelp")
|
|
ch = ""
|
|
for c in che:
|
|
scripts = c.findall("script")
|
|
paths = c.findall("path")
|
|
lists = c.findall("list")
|
|
|
|
# Current backend doesn't support multiple allowed: tags
|
|
# so we get to emulate it
|
|
comp_exprs = []
|
|
for i in lists:
|
|
comp_exprs.append("echo \"{0}\"".format(i.text))
|
|
for i in paths:
|
|
comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text))
|
|
for i in scripts:
|
|
comp_exprs.append("sh -c \"{0}\"".format(i.text))
|
|
comp_help = " && ".join(comp_exprs)
|
|
props["comp_help"] = comp_help
|
|
except:
|
|
props["comp_help"] = []
|
|
|
|
# Get priority
|
|
try:
|
|
props["priority"] = p.find("priority").text
|
|
except:
|
|
pass
|
|
|
|
# Get "multi"
|
|
if p.find("multi") is not None:
|
|
props["multi"] = True
|
|
|
|
# Get "valueless"
|
|
if p.find("valueless") is not None:
|
|
props["valueless"] = True
|
|
|
|
return props
|
|
|
|
def process_node(n, f):
|
|
|
|
|
|
props_elem = n.find("properties")
|
|
children = n.find("children")
|
|
command = n.find("command")
|
|
children_nodes = []
|
|
owner = n.get("owner")
|
|
node_type = n.tag
|
|
defaultvalue = n.find("defaultValue")
|
|
|
|
if defaultvalue is not None:
|
|
defaultvalue = defaultvalue.text
|
|
|
|
|
|
name = n.get("name")
|
|
props = get_properties(props_elem)
|
|
|
|
if node_type != "node":
|
|
if "valueless" not in props.keys():
|
|
props["type"] = "txt"
|
|
if node_type == "tagNode":
|
|
props["tag"] = "True"
|
|
|
|
if node_type == "node" and children is not None:
|
|
inner_nodes = children.iterfind("*")
|
|
index_child = 0
|
|
for inner_n in inner_nodes:
|
|
children_nodes.append(process_node(inner_n, f))
|
|
index_child = index_child + 1
|
|
|
|
if node_type == "tagNode" and children is not None:
|
|
inner_nodes = children.iterfind("*")
|
|
index_child = 0
|
|
for inner_n in inner_nodes:
|
|
children_nodes.append(process_node(inner_n, f))
|
|
index_child = index_child + 1
|
|
else:
|
|
# This is a leaf node
|
|
pass
|
|
|
|
if command is not None:
|
|
test_command = True
|
|
else:
|
|
test_command = False
|
|
node = {
|
|
'name': name,
|
|
'type': node_type,
|
|
'children': children_nodes,
|
|
'props': props,
|
|
'command': test_command,
|
|
'filename': f,
|
|
'defaultvalue': defaultvalue
|
|
|
|
}
|
|
return node
|
|
|
|
|
|
|
|
def create_commands(data, parent_list=[], level=0):
|
|
result = []
|
|
command = {
|
|
'name': [],
|
|
'help': None,
|
|
'tag_help': [],
|
|
'level': level,
|
|
'no_childs': False,
|
|
'filename': None,
|
|
'defaultvalue': None,
|
|
}
|
|
command['filename'] = data['filename']
|
|
command['defaultvalue'] = data['defaultvalue']
|
|
command['name'].extend(parent_list)
|
|
command['name'].append(data['name'])
|
|
|
|
if data['type'] == 'tagNode':
|
|
command['name'].append("<" + data['name'] + ">")
|
|
|
|
if 'val_help' in data['props'].keys():
|
|
for val_help in data['props']['val_help']:
|
|
command['tag_help'].append(val_help)
|
|
|
|
if len(data['children']) == 0:
|
|
command['no_childs'] = True
|
|
|
|
if data['command']:
|
|
command['no_childs'] = True
|
|
|
|
try:
|
|
help_text = data['props']['help']
|
|
command['help'] = re.sub(r"[\n\t]*", "", help_text)
|
|
|
|
except:
|
|
command['help'] = ""
|
|
|
|
command['valueless'] = data['props']['valueless']
|
|
|
|
if 'children' in data.keys():
|
|
children_bool = True
|
|
for child in data['children']:
|
|
result.extend(create_commands(child, command['name'], level + 1))
|
|
|
|
if command['no_childs']:
|
|
result.append(command)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
def include_file(line, input_dir):
|
|
string = ""
|
|
if "#include <include" in line.strip():
|
|
include_filename = line.strip().split('<')[1][:-1]
|
|
with open(input_dir + include_filename) as ifp:
|
|
iline = ifp.readline()
|
|
while iline:
|
|
string = string + include_file(iline.strip(), input_dir)
|
|
iline = ifp.readline()
|
|
else:
|
|
string = line
|
|
return string
|
|
|
|
|
|
def get_working_commands():
|
|
for entry in input_data:
|
|
for (dirpath, dirnames, filenames) in os.walk(entry['input_dir']):
|
|
entry['files'].extend(filenames)
|
|
break
|
|
|
|
for f in entry['files']:
|
|
|
|
string = ""
|
|
with open(entry['input_dir'] + f) as fp:
|
|
line = fp.readline()
|
|
while line:
|
|
string = string + include_file(line.strip(), entry['input_dir'])
|
|
line = fp.readline()
|
|
|
|
try:
|
|
xml = ET.parse(BytesIO(bytes(string, 'utf-8')))
|
|
except Exception as e:
|
|
print("Failed to load interface definition file {0}".format(f))
|
|
print(e)
|
|
sys.exit(1)
|
|
|
|
override_defaults(xml)
|
|
|
|
try:
|
|
relaxng_xml = ET.parse(entry['schema_file'])
|
|
validator = ET.RelaxNG(relaxng_xml)
|
|
|
|
if not validator.validate(xml):
|
|
print(validator.error_log)
|
|
print("Interface definition file {0} does not match the schema!".format(f))
|
|
#sys.exit(1)
|
|
except Exception as e:
|
|
print("Failed to load the XML schema {0}".format(entry['schema_file']))
|
|
print(e)
|
|
sys.exit(1)
|
|
|
|
root = xml.getroot()
|
|
nodes = root.iterfind("*")
|
|
for n in nodes:
|
|
node_data[entry['kind']][f] = process_node(n, f)
|
|
|
|
# build config tree and sort
|
|
|
|
config_tree_new = {
|
|
'cfgcmd': {},
|
|
'opcmd': {},
|
|
}
|
|
|
|
for kind in node_data:
|
|
for entry in node_data[kind]:
|
|
node_0 = node_data[kind][entry]['name']
|
|
|
|
if node_0 not in config_tree_new[kind].keys():
|
|
config_tree_new[kind][node_0] = {
|
|
'name': node_0,
|
|
'type': node_data[kind][entry]['type'],
|
|
'props': node_data[kind][entry]['props'],
|
|
'children': [],
|
|
'command': node_data[kind][entry]['command'],
|
|
'filename': node_data[kind][entry]['filename'],
|
|
'defaultvalue': node_data[kind][entry]['defaultvalue']
|
|
}
|
|
config_tree_new[kind][node_0]['children'].extend(node_data[kind][entry]['children'])
|
|
|
|
result = {
|
|
'cfgcmd': [],
|
|
'opcmd': [],
|
|
}
|
|
for kind in config_tree_new:
|
|
for e in config_tree_new[kind]:
|
|
if config_tree_new[kind][e]['name']:
|
|
result[kind].extend(create_commands(config_tree_new[kind][e]))
|
|
|
|
for cmd in result['cfgcmd']:
|
|
cmd['cmd'] = " ".join(cmd['name'])
|
|
for cmd in result['opcmd']:
|
|
cmd['cmd'] = " ".join(cmd['name'])
|
|
return result
|
|
|
|
def override_defaults(xml):
|
|
root = xml.getroot()
|
|
defv = {}
|
|
|
|
xpath_str = f'//defaultValue'
|
|
xp = xml.xpath(xpath_str)
|
|
|
|
for element in xp:
|
|
ap = element.xpath('ancestor::*[@name]')
|
|
defv.setdefault((ap[-1].get("name"), str(ap[:-1])), []).append(element)
|
|
|
|
for k, v in defv.items():
|
|
if len(v) > 1:
|
|
override_element(v)
|
|
|
|
def override_element(l: list):
|
|
if len(l) < 2:
|
|
return
|
|
|
|
# assemble list of leafNodes of overriding defaultValues, for later removal
|
|
parents = []
|
|
for el in l[1:]:
|
|
parents.append(el.getparent())
|
|
|
|
# replace element with final override
|
|
l[0].getparent().replace(l[0], l[-1])
|
|
|
|
# remove all but overridden element
|
|
for el in parents:
|
|
el.getparent().remove(el)
|
|
|
|
if __name__ == "__main__":
|
|
get_vyos_commands()
|