Skip to content

Commit

Permalink
T5083: extend xml schema definitions to support child requirements
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasfinstad committed Jun 2, 2024
1 parent 7925402 commit 38a1913
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 67 deletions.
43 changes: 43 additions & 0 deletions interface-definitions/container.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
<regex>[-a-zA-Z0-9]+</regex>
</constraint>
<constraintErrorMessage>Container name must be alphanumeric and can contain hyphens</constraintErrorMessage>
<childRequirements>
<require>
<child name="image"/>
</require>
<conflict name="allow-host-networks">
<child name="network"/>
</conflict>
<atLeastOneOf>
<child name="allow-host-networks"/>
<child name="network"/>
</atLeastOneOf>
<depend name="gid">
<child name="uid"/>
</depend>
</childRequirements>
</properties>
<children>
<leafNode name="allow-host-networks">
Expand Down Expand Up @@ -69,6 +84,12 @@
<tagNode name="device">
<properties>
<help>Add a host device to the container</help>
<childRequirements>
<require>
<child name="source"/>
<child name="destination"/>
</require>
</childRequirements>
</properties>
<children>
<leafNode name="source">
Expand Down Expand Up @@ -99,6 +120,11 @@
<regex>[-_a-zA-Z0-9]+</regex>
</constraint>
<constraintErrorMessage>Environment variable name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
<childRequirements>
<require>
<child name="value"/>
</require>
</childRequirements>
</properties>
<children>
<leafNode name="value">
Expand Down Expand Up @@ -170,6 +196,11 @@
<regex>[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?</regex>
</constraint>
<constraintErrorMessage>Label variable name must be alphanumeric and can contain hyphen, dots and underscores</constraintErrorMessage>
<childRequirements>
<require>
<child name="value"/>
</require>
</childRequirements>
</properties>
<children>
<leafNode name="value">
Expand Down Expand Up @@ -253,6 +284,12 @@
<tagNode name="port">
<properties>
<help>Publish port to the container</help>
<childRequirements>
<require>
<child name="source"/>
<child name="destination"/>
</require>
</childRequirements>
</properties>
<children>
#include <include/listen-address.xml.i>
Expand Down Expand Up @@ -361,6 +398,12 @@
<tagNode name="volume">
<properties>
<help>Mount a volume into the container</help>
<childRequirements>
<require>
<child name="source"/>
<child name="destination"/>
</require>
</childRequirements>
</properties>
<children>
<leafNode name="source">
Expand Down
22 changes: 21 additions & 1 deletion python/vyos/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@

import re
import json
from typing import Union
from typing import Union, Any
from copy import deepcopy

import vyos.configtree
from vyos.xml_ref import multi_to_list
Expand All @@ -79,12 +80,24 @@
class ConfigDict(dict):
_from_defaults = {}
_dict_kwargs = {}
_raw_conf_dict: dict[str, Any]
_base: list[str]

def from_defaults(self, path: list[str]) -> bool:
return from_source(self._from_defaults, path)

@property
def kwargs(self) -> dict:
return self._dict_kwargs

@property
def raw_conf_dict(self):
return self._raw_conf_dict

@property
def base(self):
return self._base

def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict:
if not isinstance(dest, ConfigDict):
dest = ConfigDict(dest)
Expand Down Expand Up @@ -312,6 +325,8 @@ def get_config_dict(self, path=[], effective=False, key_mangling=None,
root_dict = self.get_cached_root_dict(effective)
conf_dict = get_sub_dict(root_dict, lpath, get_first_key=get_first_key)

raw_conf_dict = deepcopy(conf_dict)

rpath = lpath if get_first_key else lpath[:-1]

if not no_multi_convert:
Expand Down Expand Up @@ -346,6 +361,11 @@ def get_config_dict(self, path=[], effective=False, key_mangling=None,
# save optional args for a call to get_config_defaults
setattr(conf_dict, '_dict_kwargs', kwargs)

# save args that are reused during verification
setattr(conf_dict, '_raw_conf_dict', raw_conf_dict)
setattr(conf_dict, '_base', rpath)


return conf_dict

def get_config_defaults(self, path=[], effective=False, key_mangling=None,
Expand Down
53 changes: 53 additions & 0 deletions python/vyos/configverify.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,62 @@

from vyos import ConfigError
from vyos.utils.dict import dict_search
from vyos.xml_ref import is_leaf, is_tag, is_tag_value
# pattern re-used in ipsec migration script
dynamic_interface_pattern = r'(ppp|pppoe|sstpc|l2tp|ipoe)[0-9]+'

def verify_children(rpath: list[str], config: dict, key_mangling=None):
from vyos.xml_ref import child_requirements

# Path without tagnode values so it can be found in the xml definitions
# eg: [container name my-container environment myVariable] becomes [container name environment]
xml_path = []
for i in range(len(rpath)):
if not is_tag_value(rpath[:i+1]):
xml_path.append(rpath[i])

if is_tag(xml_path) and not is_tag_value(rpath):
for k, v in config.items():
verify_children(rpath + [k], v)
return

if is_leaf(rpath):
return

schema_requirements = child_requirements(rpath)

for requirement in schema_requirements:
match requirement[0]:
case "require":
for req in requirement[1]:
if req not in config:
raise ConfigError(f'[{" ".join(rpath)}] Requires "{req}" to be configured')

case "atLeastOneOf":
for req in requirement[1]:
if req in config:
break
else:
children = '" or "'.join(requirement[1])
raise ConfigError(f'[{" ".join(rpath)}] Requires at least one of "{children}" to be configured')

case "depend":
if (requirement[1][0] in config) and not (requirement[1][1] in config):
raise ConfigError(f'[{" ".join(rpath)}] Can not configure "{requirement[1][0]}" without "{requirement[1][1]}"')

case "conflict":
if (requirement[1][0] in config) and (requirement[1][1] in config):
raise ConfigError(f'[{" ".join(rpath)}] "{requirement[1][0]}" and "{requirement[1][1]}" can not be configured at the same time')

case _:
raise ValueError("Unsupported child requirement type")

for k, requirement in config.items():
path = rpath.copy()
path.append(k)
verify_children(path, requirement)


def verify_mtu(config):
"""
Common helper function used by interface implementations to perform
Expand Down
3 changes: 3 additions & 0 deletions python/vyos/xml_ref/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def owner(path: list) -> str:
def priority(path: list) -> str:
return load_reference().priority(path)

def child_requirements(path: list[str]) -> list:
return load_reference().child_requirements(path)

def cli_defined(path: list, node: str, non_local=False) -> bool:
return load_reference().cli_defined(path, node, non_local=non_local)

Expand Down
4 changes: 4 additions & 0 deletions python/vyos/xml_ref/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ def owner(self, path: list) -> str:
def priority(self, path: list) -> str:
return self._least_upper_data(path, 'priority')

def child_requirements(self, path: list[str]) -> list:
d = self._get_ref_path(path)
return self._get_ref_node_data(d, 'child_requirements')

@staticmethod
def _dict_get(d: dict, path: list) -> dict:
for i in path:
Expand Down
2 changes: 1 addition & 1 deletion python/vyos/xml_ref/generate_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
ref_cache = abspath(join(_here, 'cache.py'))

node_data_fields = ("node_type", "multi", "valueless", "default_value",
"owner", "priority")
"owner", "priority", "child_requirements")

def trim_node_data(cache: dict):
for k in list(cache):
Expand Down
17 changes: 16 additions & 1 deletion schema/interface_definition.rnc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# USA

# The language of this file is compact form RELAX-NG
# http://relaxng.org/compact-tutorial-20030326.htm
# https://relaxng.org/compact-tutorial-20030326.html
# (unless converted to XML, then just RELAX-NG :)

# Interface definition starts with interfaceDefinition tag that may contain node tags
Expand Down Expand Up @@ -105,6 +105,9 @@ properties = element properties
(element secret { empty })? &
(element priority { text })? &

# These are meaningful only for tag and node nodes
childRequirements? &

# These are meaningful only for tag nodes
(element keepChildOrder { empty })?
}
Expand Down Expand Up @@ -184,3 +187,15 @@ completionHelp = element completionHelp
(element path { text })* &
(element script { text })*
}


# childRequirements tags is a declarative way to configure basic
# requirements of node or tagnode children.
childRequirements = element childRequirements {
(element require { child+ } )? &
(element conflict { nodeNameAttr, child+ })* &
(element atLeastOneOf { child+ } )* &
(element depend { nodeNameAttr, child+ })*
}

child = element child {nodeNameAttr,empty}
64 changes: 57 additions & 7 deletions schema/interface_definition.rng
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<!--
interface_definition.rnc: VyConf reference tree XML grammar
Copyright (C) 2014. 2017 VyOS maintainers and contributors <[email protected]>
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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
-->
<!--
The language of this file is compact form RELAX-NG
http://relaxng.org/compact-tutorial-20030326.htm
https://relaxng.org/compact-tutorial-20030326.html
(unless converted to XML, then just RELAX-NG :)
-->
<!-- Interface definition starts with interfaceDefinition tag that may contain node tags -->
Expand Down Expand Up @@ -142,15 +142,15 @@
Nodes may have properties
For simplicity, any property is allowed in any node,
but whether they are used or not is implementation-defined
Leaf nodes may differ in number of values that can be
associated with them.
By default, a leaf node can have only one value.
"multi" tag means a node can have one or more values,
"valueless" means it can have no values at all.
"hidden" means node visibility can be toggled, eg 'dangerous' commands,
"secret" allows a node to hide its value from unprivileged users.
"priority" is used to influence node processing order for nodes
with exact same dependencies and in compatibility modes.
-->
Expand Down Expand Up @@ -205,6 +205,10 @@
<text/>
</element>
</optional>
<optional>
<!-- These are meaningful only for tag and node nodes -->
<ref name="childRequirements"/>
</optional>
<optional>
<!-- These are meaningful only for tag nodes -->
<group>
Expand Down Expand Up @@ -328,4 +332,50 @@
</interleave>
</element>
</define>
<!--
childRequirements tags is a declarative way to configure basic
requirements of node or tagnode children.
-->
<define name="childRequirements">
<element name="childRequirements">
<interleave>
<optional>
<element name="require">
<oneOrMore>
<ref name="child"/>
</oneOrMore>
</element>
</optional>
<zeroOrMore>
<element name="conflict">
<ref name="nodeNameAttr"/>
<oneOrMore>
<ref name="child"/>
</oneOrMore>
</element>
</zeroOrMore>
<zeroOrMore>
<element name="atLeastOneOf">
<oneOrMore>
<ref name="child"/>
</oneOrMore>
</element>
</zeroOrMore>
<zeroOrMore>
<element name="depend">
<ref name="nodeNameAttr"/>
<oneOrMore>
<ref name="child"/>
</oneOrMore>
</element>
</zeroOrMore>
</interleave>
</element>
</define>
<define name="child">
<element name="child">
<ref name="nodeNameAttr"/>
<empty/>
</element>
</define>
</grammar>
5 changes: 3 additions & 2 deletions schema/op-mode-definition.rng
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,11 @@
<!--
completionHelp tags contain information about allowed values of a node that is used for generating
tab completion in the CLI frontend and drop-down lists in GUI frontends
It is only meaninful for leaf nodes
It is only meaningful for leaf nodes
Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>),
as a configuration path (e.g. <path>interfaces ethernet</path>),
or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>
as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script>,
or to enable built-in image path completion (<imagePath/>).
-->
<define name="completionHelp">
<element name="completionHelp">
Expand Down
Loading

0 comments on commit 38a1913

Please sign in to comment.