summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/base.py3
-rw-r--r--python/vyos/ethtool.py15
-rw-r--r--python/vyos/ifconfig/ethernet.py14
-rw-r--r--python/vyos/opmode.py5
-rw-r--r--python/vyos/template.py1
-rw-r--r--python/vyos/utils/__init__.py0
-rw-r--r--python/vyos/utils/dict.py235
-rw-r--r--python/vyos/xml/load.py18
8 files changed, 272 insertions, 19 deletions
diff --git a/python/vyos/base.py b/python/vyos/base.py
index 9b93cb2f2..c1acfd060 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2018-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
@@ -41,7 +41,6 @@ class BaseWarning:
isfirstmessage = False
initial_indent = self.standardindent
print(f'{mes}')
- print('')
class Warning():
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index bc3402059..1b1e54dfb 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -51,6 +51,7 @@ class Ethtool:
_ring_buffers_max = { }
_driver_name = None
_auto_negotiation = False
+ _auto_negotiation_supported = None
_flow_control = False
_flow_control_enabled = None
@@ -80,7 +81,13 @@ class Ethtool:
self._speed_duplex.update({ speed : {}})
if duplex not in self._speed_duplex[speed]:
self._speed_duplex[speed].update({ duplex : ''})
- if 'Auto-negotiation:' in line:
+ if 'Supports auto-negotiation:' in line:
+ # Split the following string: Auto-negotiation: off
+ # we are only interested in off or on
+ tmp = line.split()[-1]
+ self._auto_negotiation_supported = bool(tmp == 'Yes')
+ # Only read in if Auto-negotiation is supported
+ if self._auto_negotiation_supported and 'Auto-negotiation:' in line:
# Split the following string: Auto-negotiation: off
# we are only interested in off or on
tmp = line.split()[-1]
@@ -132,8 +139,12 @@ class Ethtool:
# ['Autonegotiate:', 'on']
self._flow_control_enabled = out.splitlines()[1].split()[-1]
+ def check_auto_negotiation_supported(self):
+ """ Check if the NIC supports changing auto-negotiation """
+ return self._auto_negotiation_supported
+
def get_auto_negotiation(self):
- return self._auto_negotiation
+ return self._auto_negotiation_supported and self._auto_negotiation
def get_driver_name(self):
return self._driver_name
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 5080144ff..6a49c022a 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-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
@@ -14,9 +14,10 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
from glob import glob
+
+from vyos.base import Warning
from vyos.ethtool import Ethtool
from vyos.ifconfig.interface import Interface
from vyos.util import run
@@ -118,7 +119,7 @@ class EthernetIf(Interface):
cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
output, code = self._popen(cmd)
if code:
- print(f'Could not set flowcontrol for {ifname}')
+ Warning(f'could not change "{ifname}" flow control setting!')
return output
return None
@@ -134,6 +135,7 @@ class EthernetIf(Interface):
>>> i = EthernetIf('eth0')
>>> i.set_speed_duplex('auto', 'auto')
"""
+ ifname = self.config['ifname']
if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000',
'25000', '40000', '50000', '100000', '400000']:
@@ -143,7 +145,11 @@ class EthernetIf(Interface):
raise ValueError("Value out of range (duplex)")
if not self.ethtool.check_speed_duplex(speed, duplex):
- self._debug_msg(f'NIC driver does not support changing speed/duplex settings!')
+ Warning(f'changing speed/duplex setting on "{ifname}" is unsupported!')
+ return
+
+ if not self.ethtool.check_auto_negotiation_supported():
+ Warning(f'changing auto-negotiation setting on "{ifname}" is unsupported!')
return
# Get current speed and duplex settings:
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index d7172a0b5..230a85541 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -209,6 +209,11 @@ def run(module):
for opt in type_hints:
th = type_hints[opt]
+ # Function argument names use underscores as separators
+ # but command-line options should use hyphens
+ # Without this, we'd get options like "--foo_bar"
+ opt = re.sub(r'_', '-', opt)
+
if _get_arg_type(th) == bool:
subparser.add_argument(f"--{opt}", action='store_true')
else:
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 06a292706..254a15e3a 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -44,6 +44,7 @@ def _get_environment(location=None):
loader=loc_loader,
trim_blocks=True,
undefined=ChainableUndefined,
+ extensions=['jinja2.ext.loopcontrols']
)
env.filters.update(_FILTERS)
env.tests.update(_TESTS)
diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/utils/__init__.py
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
new file mode 100644
index 000000000..66b40d92b
--- /dev/null
+++ b/python/vyos/utils/dict.py
@@ -0,0 +1,235 @@
+# 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
+# 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/>.
+
+
+def colon_separated_to_dict(data_string, uniquekeys=False):
+ """ Converts a string containing newline-separated entries
+ of colon-separated key-value pairs into a dict.
+
+ Such files are common in Linux /proc filesystem
+
+ Args:
+ data_string (str): data string
+ uniquekeys (bool): whether to insist that keys are unique or not
+
+ Returns: dict
+
+ Raises:
+ ValueError: if uniquekeys=True and the data string has
+ duplicate keys.
+
+ Note:
+ If uniquekeys=True, then dict entries are always strings,
+ otherwise they are always lists of strings.
+ """
+ import re
+ key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
+
+ data_raw = re.split('\n', data_string)
+
+ data = {}
+
+ for l in data_raw:
+ l = l.strip()
+ if l:
+ match = re.match(key_value_re, l)
+ if match and (len(match.groups()) == 2):
+ key = match.groups()[0].strip()
+ value = match.groups()[1].strip()
+ else:
+ raise ValueError(f"""Line "{l}" could not be parsed a colon-separated pair """, l)
+ if key in data.keys():
+ if uniquekeys:
+ raise ValueError("Data string has duplicate keys: {0}".format(key))
+ else:
+ data[key].append(value)
+ else:
+ if uniquekeys:
+ data[key] = value
+ else:
+ data[key] = [value]
+ else:
+ pass
+
+ return data
+
+def _mangle_dict_keys(data, regex, replacement, abs_path=[], no_tag_node_value_mangle=False, mod=0):
+ """ Mangles dict keys according to a regex and replacement character.
+ Some libraries like Jinja2 do not like certain characters in dict keys.
+ This function can be used for replacing all offending characters
+ with something acceptable.
+
+ Args:
+ data (dict): Original dict to mangle
+
+ Returns: dict
+ """
+ from vyos.xml import is_tag
+
+ new_dict = {}
+
+ for key in data.keys():
+ save_mod = mod
+ save_path = abs_path[:]
+
+ abs_path.append(key)
+
+ if not is_tag(abs_path):
+ new_key = re.sub(regex, replacement, key)
+ else:
+ if mod%2:
+ new_key = key
+ else:
+ new_key = re.sub(regex, replacement, key)
+ if no_tag_node_value_mangle:
+ mod += 1
+
+ value = data[key]
+
+ if isinstance(value, dict):
+ new_dict[new_key] = _mangle_dict_keys(value, regex, replacement, abs_path=abs_path, mod=mod, no_tag_node_value_mangle=no_tag_node_value_mangle)
+ else:
+ new_dict[new_key] = value
+
+ mod = save_mod
+ abs_path = save_path[:]
+
+ return new_dict
+
+def mangle_dict_keys(data, regex, replacement, abs_path=[], no_tag_node_value_mangle=False):
+ return _mangle_dict_keys(data, regex, replacement, abs_path=abs_path, no_tag_node_value_mangle=no_tag_node_value_mangle, mod=0)
+
+def _get_sub_dict(d, lpath):
+ k = lpath[0]
+ if k not in d.keys():
+ return {}
+ c = {k: d[k]}
+ lpath = lpath[1:]
+ if not lpath:
+ return c
+ elif not isinstance(c[k], dict):
+ return {}
+ return _get_sub_dict(c[k], lpath)
+
+def get_sub_dict(source, lpath, get_first_key=False):
+ """ Returns the sub-dict of a nested dict, defined by path of keys.
+
+ Args:
+ source (dict): Source dict to extract from
+ lpath (list[str]): sequence of keys
+
+ Returns: source, if lpath is empty, else
+ {key : source[..]..[key]} for key the last element of lpath, if exists
+ {} otherwise
+ """
+ if not isinstance(source, dict):
+ raise TypeError("source must be of type dict")
+ if not isinstance(lpath, list):
+ raise TypeError("path must be of type list")
+ if not lpath:
+ return source
+
+ ret = _get_sub_dict(source, lpath)
+
+ if get_first_key and lpath and ret:
+ tmp = next(iter(ret.values()))
+ if not isinstance(tmp, dict):
+ raise TypeError("Data under node is not of type dict")
+ ret = tmp
+
+ return ret
+
+def dict_search(path, dict_object):
+ """ Traverse Python dictionary (dict_object) delimited by dot (.).
+ Return value of key if found, None otherwise.
+
+ This is faster implementation then jmespath.search('foo.bar', dict_object)"""
+ if not isinstance(dict_object, dict) or not path:
+ return None
+
+ parts = path.split('.')
+ inside = parts[:-1]
+ if not inside:
+ if path not in dict_object:
+ return None
+ return dict_object[path]
+ c = dict_object
+ for p in parts[:-1]:
+ c = c.get(p, {})
+ return c.get(parts[-1], None)
+
+def dict_search_args(dict_object, *path):
+ # Traverse dictionary using variable arguments
+ # Added due to above function not allowing for '.' in the key names
+ # Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...)
+ if not isinstance(dict_object, dict) or not path:
+ return None
+
+ for item in path:
+ if item not in dict_object:
+ return None
+ dict_object = dict_object[item]
+ return dict_object
+
+def dict_search_recursive(dict_object, key, path=[]):
+ """ Traverse a dictionary recurisvely and return the value of the key
+ we are looking for.
+
+ Thankfully copied from https://stackoverflow.com/a/19871956
+
+ Modified to yield optional path to found keys
+ """
+ if isinstance(dict_object, list):
+ for i in dict_object:
+ new_path = path + [i]
+ for x in dict_search_recursive(i, key, new_path):
+ yield x
+ elif isinstance(dict_object, dict):
+ if key in dict_object:
+ new_path = path + [key]
+ yield dict_object[key], new_path
+ for k, j in dict_object.items():
+ new_path = path + [k]
+ for x in dict_search_recursive(j, key, new_path):
+ yield x
+
+def dict_to_list(d, save_key_to=None):
+ """ Convert a dict to a list of dicts.
+
+ Optionally, save the original key of the dict inside
+ dicts stores in that list.
+ """
+ def save_key(i, k):
+ if isinstance(i, dict):
+ i[save_key_to] = k
+ return
+ elif isinstance(i, list):
+ for _i in i:
+ save_key(_i, k)
+ else:
+ raise ValueError(f"Cannot save the key: the item is {type(i)}, not a dict")
+
+ collect = []
+
+ for k,_ in d.items():
+ item = d[k]
+ if save_key_to is not None:
+ save_key(item, k)
+ if isinstance(item, list):
+ collect += item
+ else:
+ collect.append(item)
+
+ return collect
diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py
index c3022f3d6..f842ff9ce 100644
--- a/python/vyos/xml/load.py
+++ b/python/vyos/xml/load.py
@@ -71,16 +71,12 @@ def _merge(dict1, dict2):
continue
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
dict1[k] = _merge(dict1[k], dict2[k])
- elif isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ elif isinstance(dict1[k], list) and isinstance(dict2[k], list):
dict1[k].extend(dict2[k])
elif dict1[k] == dict2[k]:
- # A definition shared between multiple files
- if k in (kw.valueless, kw.multi, kw.hidden, kw.node, kw.summary, kw.owner, kw.priority):
- continue
- _fatal()
- raise RuntimeError('parsing issue - undefined leaf?')
+ continue
else:
- raise RuntimeError('parsing issue - we messed up?')
+ dict1[k] = dict2[k]
return dict1
@@ -131,7 +127,7 @@ def _format_nodes(inside, conf, xml):
name = node.pop('@name')
into = inside + [name]
if name in r:
- r[name].update(_format_node(into, node, xml))
+ _merge(r[name], _format_node(into, node, xml))
else:
r[name] = _format_node(into, node, xml)
r[name][kw.node] = nodename
@@ -141,7 +137,7 @@ def _format_nodes(inside, conf, xml):
name = node.pop('@name')
into = inside + [name]
if name in r:
- r[name].update(_format_node(inside + [name], node, xml))
+ _merge(r[name], _format_node(inside + [name], node, xml))
else:
r[name] = _format_node(inside + [name], node, xml)
r[name][kw.node] = nodename
@@ -180,10 +176,10 @@ def _format_node(inside, conf, xml):
if isinstance(conf, list):
for child in children:
- r = _safe_update(r, _format_nodes(inside, child, xml))
+ _merge(r, _format_nodes(inside, child, xml))
else:
child = children
- r = _safe_update(r, _format_nodes(inside, child, xml))
+ _merge(r, _format_nodes(inside, child, xml))
elif 'properties' in keys:
properties = conf.pop('properties')