Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xml: T6650: add initial op-mode cache support #3971

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ data/component-versions.json
# vyos-1x XML cache
python/vyos/xml_ref/cache.py
python/vyos/xml_ref/pkg_cache/*_cache.py
python/vyos/xml_ref/op_cache.py
python/vyos/xml_ref/pkg_cache/*_op_cache.py
# autogenerated vyos-configd JSON definition
data/configd-include.json

Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ op_mode_definitions: $(op_xml_obj)

find $(BUILD_DIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1

$(CURDIR)/python/vyos/xml_ref/generate_op_cache.py --xml-dir $(BUILD_DIR)/op-mode-definitions || exit 1

# XXX: tcpdump, ping, traceroute and mtr must be able to recursivly call themselves as the
# options are provided from the scripts themselves
ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/
Expand Down
3 changes: 2 additions & 1 deletion python/vyos/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
'vyos_udev_dir' : '/run/udev/vyos',
'isc_dhclient_dir' : '/run/dhclient',
'dhcp6_client_dir' : '/run/dhcp6c',
'vyos_configdir' : '/opt/vyatta/config'
'vyos_configdir' : '/opt/vyatta/config',
'completion_dir' : f'{base_dir}/completion'
}

config_status = '/tmp/vyos-config-status'
Expand Down
23 changes: 23 additions & 0 deletions python/vyos/xml_ref/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from typing import Optional, Union, TYPE_CHECKING
from vyos.xml_ref import definition
from vyos.xml_ref import op_definition

if TYPE_CHECKING:
from vyos.config import ConfigDict
Expand Down Expand Up @@ -87,3 +88,25 @@ def from_source(d: dict, path: list) -> bool:

def ext_dict_merge(source: dict, destination: Union[dict, 'ConfigDict']):
return definition.ext_dict_merge(source, destination)

def load_op_reference(op_cache=[]):
if op_cache:
return op_cache[0]

op_xml = op_definition.OpXml()

try:
from vyos.xml_ref.op_cache import op_reference
except Exception:
raise ImportError('no xml op reference cache !!')

if not op_reference:
raise ValueError('empty xml op reference cache !!')

op_xml.define(op_reference)
op_cache.append(op_xml)

return op_xml

def get_op_ref_path(path: list) -> list[op_definition.PathData]:
return load_op_reference()._get_op_ref_path(path)
176 changes: 176 additions & 0 deletions python/vyos/xml_ref/generate_op_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
#
# Copyright (C) 2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#

import re
import sys
import json
import glob
from argparse import ArgumentParser
from argparse import ArgumentTypeError
from os.path import join
from os.path import abspath
from os.path import dirname
from xml.etree import ElementTree as ET
from xml.etree.ElementTree import Element
from typing import TypeAlias
from typing import Optional

_here = dirname(__file__)

sys.path.append(join(_here, '..'))
from defaults import directories

from op_definition import NodeData
from op_definition import PathData

xml_op_cache_json = 'xml_op_cache.json'
xml_op_tmp = join('/tmp', xml_op_cache_json)
op_ref_cache = abspath(join(_here, 'op_cache.py'))

OptElement: TypeAlias = Optional[Element]
DEBUG = False


def translate_exec(s: str) -> str:
s = s.replace('${vyos_op_scripts_dir}', directories['op_mode'])
s = s.replace('${vyos_libexec_dir}', directories['base'])
return s


def translate_position(s: str, pos: list[str]) -> str:
pos = pos.copy()
pat: re.Pattern = re.compile(r'(?:\")?\${?([0-9]+)}?(?:\")?')
t: str = pat.sub(r'_place_holder_\1_', s)

# preferred to .format(*list) to avoid collisions with braces
for i, p in enumerate(pos):
t = t.replace(f'_place_holder_{i+1}_', p)

return t


def translate_command(s: str, pos: list[str]) -> str:
s = translate_exec(s)
s = translate_position(s, pos)
return s


def translate_op_script(s: str) -> str:
s = s.replace('${vyos_completion_dir}', directories['completion_dir'])
s = s.replace('${vyos_op_scripts_dir}', directories['op_mode'])
return s


def insert_node(n: Element, l: list[PathData], path = None) -> None:
# pylint: disable=too-many-locals,too-many-branches
prop: OptElement = n.find('properties')
children: OptElement = n.find('children')
command: OptElement = n.find('command')
# name is not None as required by schema
name: str = n.get('name', 'schema_error')
node_type: str = n.tag
if path is None:
path = []

path.append(name)
if node_type == 'tagNode':
path.append(f'{name}-tag_value')

help_prop: OptElement = None if prop is None else prop.find('help')
help_text = None if help_prop is None else help_prop.text
command_text = None if command is None else command.text
if command_text is not None:
command_text = translate_command(command_text, path)

comp_help = None
if prop is not None:
che = prop.findall("completionHelp")
for c in che:
lists = c.findall("list")
paths = c.findall("path")
scripts = c.findall("script")

comp_help = {}
list_l = []
for i in lists:
list_l.append(i.text)
path_l = []
for i in paths:
path_str = re.sub(r'\s+', '/', i.text)
path_l.append(path_str)
script_l = []
for i in scripts:
script_str = translate_op_script(i.text)
script_l.append(script_str)

comp_help['list'] = list_l
comp_help['fs_path'] = path_l
comp_help['script'] = script_l

for d in l:
if name in list(d):
break
else:
d = {}
l.append(d)

inner_l = d.setdefault(name, [])

inner_d: PathData = {'node_data': NodeData(node_type=node_type,
help_text=help_text,
comp_help=comp_help,
command=command_text,
path=path)}
inner_l.append(inner_d)

if children is not None:
inner_nodes = children.iterfind("*")
for inner_n in inner_nodes:
inner_path = path[:]
insert_node(inner_n, inner_l, inner_path)


def parse_file(file_path, l):
tree = ET.parse(file_path)
root = tree.getroot()
for n in root.iterfind("*"):
insert_node(n, l)


def main():
parser = ArgumentParser(description='generate dict from xml defintions')
parser.add_argument('--xml-dir', type=str, required=True,
help='transcluded xml op-mode-definition file')

args = vars(parser.parse_args())

xml_dir = abspath(args['xml_dir'])

l = []

for fname in glob.glob(f'{xml_dir}/*.xml'):
parse_file(fname, l)

with open(xml_op_tmp, 'w') as f:
json.dump(l, f, indent=2)

with open(op_ref_cache, 'w') as f:
f.write(f'op_reference = {str(l)}')

if __name__ == '__main__':
main()
49 changes: 49 additions & 0 deletions python/vyos/xml_ref/op_definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>.

from typing import TypedDict
from typing import TypeAlias
from typing import Optional
from typing import Union


class NodeData(TypedDict):
node_type: Optional[str]
help_text: Optional[str]
comp_help: Optional[dict[str, list]]
command: Optional[str]
path: Optional[list[str]]


PathData: TypeAlias = dict[str, Union[NodeData|list['PathData']]]


class OpXml:
def __init__(self):
self.op_ref = {}

def define(self, op_ref: list[PathData]) -> None:
self.op_ref = op_ref

def _get_op_ref_path(self, path: list[str]) -> list[PathData]:
def _get_path_list(path: list[str], l: list[PathData]) -> list[PathData]:
if not path:
return l
for d in l:
if path[0] in list(d):
return _get_path_list(path[1:], d[path[0]])
return []
l = self.op_ref
return _get_path_list(path, l)
Loading