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

T5412: Add support for extending config-mode dependencies in add-on packages (backport #2216) #2315

Merged
merged 3 commits into from
Sep 28, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 54 additions & 7 deletions python/vyos/configdep.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
# Copyright 2023 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
Expand All @@ -17,8 +17,10 @@
import json
import typing
from inspect import stack
from graphlib import TopologicalSorter, CycleError

from vyos.utils.system import load_as_module
from vyos.configdict import dict_merge
from vyos.defaults import directories
from vyos.configsource import VyOSError
from vyos import ConfigError
Expand All @@ -28,6 +30,9 @@
if typing.TYPE_CHECKING:
from vyos.config import Config

dependency_dir = os.path.join(directories['data'],
'config-mode-dependencies')

dependent_func: dict[str, list[typing.Callable]] = {}

def canon_name(name: str) -> str:
Expand All @@ -40,12 +45,20 @@ def canon_name_of_path(path: str) -> str:
def caller_name() -> str:
return stack()[-1].filename

def read_dependency_dict() -> dict:
path = os.path.join(directories['data'],
'config-mode-dependencies.json')
with open(path) as f:
d = json.load(f)
return d
def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict:
res = {}
for dep_file in os.listdir(dependency_dir):
if not dep_file.endswith('.json'):
continue
path = os.path.join(dependency_dir, dep_file)
with open(path) as f:
d = json.load(f)
if dep_file == 'vyos-1x.json':
res = dict_merge(res, d)
else:
res = dict_merge(d, res)

return res

def get_dependency_dict(config: 'Config') -> dict:
if hasattr(config, 'cached_dependency_dict'):
Expand Down Expand Up @@ -93,3 +106,37 @@ def call_dependents():
while l:
f = l.pop(0)
f()

def graph_from_dependency_dict(d: dict) -> dict:
g = {}
for k in list(d):
g[k] = set()
# add the dependencies for every sub-case; should there be cases
# that are mutally exclusive in the future, the graphs will be
# distinguished
for el in list(d[k]):
g[k] |= set(d[k][el])

return g

def is_acyclic(d: dict) -> bool:
g = graph_from_dependency_dict(d)
ts = TopologicalSorter(g)
try:
# get node iterator
order = ts.static_order()
# try iteration
_ = [*order]
except CycleError:
return False

return True

def check_dependency_graph(dependency_dir: str = dependency_dir,
supplement: str = None) -> bool:
d = read_dependency_dict(dependency_dir=dependency_dir)
if supplement is not None:
with open(supplement) as f:
d = dict_merge(json.load(f), d)

return is_acyclic(d)
54 changes: 0 additions & 54 deletions smoketest/scripts/cli/test_dependency_graph.py

This file was deleted.

58 changes: 58 additions & 0 deletions src/helpers/config_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
#
# Copyright (C) 2023 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 os
import sys
from argparse import ArgumentParser
from argparse import ArgumentTypeError

try:
from vyos.configdep import check_dependency_graph
from vyos.defaults import directories
except ImportError:
# allow running during addon package build
_here = os.path.dirname(__file__)
sys.path.append(os.path.join(_here, '../../python/vyos'))
from configdep import check_dependency_graph
from defaults import directories

# addon packages will need to specify the dependency directory
dependency_dir = os.path.join(directories['data'],
'config-mode-dependencies')

def path_exists(s):
if not os.path.exists(s):
raise ArgumentTypeError("Must specify a valid vyos-1x dependency directory")
return s

def main():
parser = ArgumentParser(description='generate and save dict from xml defintions')
parser.add_argument('--dependency-dir', type=path_exists,
default=dependency_dir,
help='location of vyos-1x dependency directory')
parser.add_argument('--supplement', type=str,
help='supplemental dependency file')
args = vars(parser.parse_args())

if not check_dependency_graph(**args):
sys.exit(1)

sys.exit(0)

if __name__ == '__main__':
main()
31 changes: 31 additions & 0 deletions src/tests/test_dependency_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
#
# Copyright (C) 2023 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 os
from vyos.configdep import check_dependency_graph

_here = os.path.dirname(__file__)
ddir = os.path.join(_here, '../../data/config-mode-dependencies')

from unittest import TestCase

class TestDependencyGraph(TestCase):
def setUp(self):
pass

def test_acyclic(self):
res = check_dependency_graph(dependency_dir=ddir)
self.assertTrue(res)