diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/component_version.py | 63 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 55 | ||||
| -rw-r--r-- | python/vyos/kea.py | 2 | ||||
| -rw-r--r-- | python/vyos/proto/vyconf_client.py | 4 | ||||
| -rw-r--r-- | python/vyos/vyconf_session.py | 123 | 
5 files changed, 221 insertions, 26 deletions
| diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py index 94215531d..81d986658 100644 --- a/python/vyos/component_version.py +++ b/python/vyos/component_version.py @@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')  REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)'  REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)'  REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*' -REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +REGEX_COMPONENT_VERSION_VYATTA = ( +    r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/' +)  REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*'  REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/' @@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\  warn_filter_vyos = re.compile(REGEX_WARN_VYOS)  warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA) -regex_filter = { 'vyos': dict(zip(['component', 'release'], -                                  [re.compile(REGEX_COMPONENT_VERSION_VYOS), -                                   re.compile(REGEX_RELEASE_VERSION_VYOS)])), -                 'vyatta': dict(zip(['component', 'release'], -                                    [re.compile(REGEX_COMPONENT_VERSION_VYATTA), -                                     re.compile(REGEX_RELEASE_VERSION_VYATTA)])) } +regex_filter = { +    'vyos': dict( +        zip( +            ['component', 'release'], +            [ +                re.compile(REGEX_COMPONENT_VERSION_VYOS), +                re.compile(REGEX_RELEASE_VERSION_VYOS), +            ], +        ) +    ), +    'vyatta': dict( +        zip( +            ['component', 'release'], +            [ +                re.compile(REGEX_COMPONENT_VERSION_VYATTA), +                re.compile(REGEX_RELEASE_VERSION_VYATTA), +            ], +        ) +    ), +} +  @dataclass  class VersionInfo: -    component: Optional[dict[str,int]] = None +    component: Optional[dict[str, int]] = None      release: str = get_version()      vintage: str = 'vyos'      config_body: Optional[str] = None @@ -84,8 +101,9 @@ class VersionInfo:          return bool(self.config_body is None)      def update_footer(self): -        f = CONFIG_FILE_VERSION.format(component_to_string(self.component), -                                       self.release) +        f = CONFIG_FILE_VERSION.format( +            component_to_string(self.component), self.release +        )          self.footer_lines = f.splitlines()      def update_syntax(self): @@ -121,13 +139,16 @@ class VersionInfo:          except Exception as e:              raise ValueError(e) from e +  def component_to_string(component: dict) -> str: -    l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] +    l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])]  # noqa: E741      return ':'.join(l) +  def component_from_string(string: str) -> dict:      return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)} +  def version_info_from_file(config_file) -> VersionInfo:      """Return config file component and release version info."""      version_info = VersionInfo() @@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo:      return version_info +  def version_info_from_system() -> VersionInfo:      """Return system component and release version info."""      d = component_version()      sort_d = dict(sorted(d.items(), key=lambda x: x[0])) -    version_info = VersionInfo( -        component = sort_d, -        release =  get_version(), -        vintage = 'vyos' -    ) +    version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos')      return version_info +  def version_info_copy(v: VersionInfo) -> VersionInfo:      """Make a copy of dataclass."""      return replace(v) +  def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo:      """In place pruning of component keys of x not in y."""      if x.component is None or y.component is None:          return -    x.component = { k: v for k,v in x.component.items() if k in y.component } +    x.component = {k: v for k, v in x.component.items() if k in y.component} +  def add_system_version(config_str: str = None, out_file: str = None):      """Wrap config string with system version and write to out_file. @@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None):          version_info.write(out_file)      else:          sys.stdout.write(version_info.write_string()) + + +def append_system_version(file: str): +    """Append system version data to existing file""" +    version_info = version_info_from_system() +    version_info.update_footer() +    with open(file, 'a') as f: +        f.write(version_info.write_string()) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 90b96b88c..a3be29881 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -21,6 +21,10 @@ import subprocess  from vyos.defaults import directories  from vyos.utils.process import is_systemd_service_running  from vyos.utils.dict import dict_to_paths +from vyos.utils.boot import boot_configuration_complete +from vyos.vyconf_session import VyconfSession + +vyconf_backend = False  CLI_SHELL_API = '/bin/cli-shell-api'  SET = '/opt/vyatta/sbin/my_set' @@ -165,6 +169,11 @@ class ConfigSession(object):          self.__run_command([CLI_SHELL_API, 'setupSession']) +        if vyconf_backend and boot_configuration_complete(): +            self._vyconf_session = VyconfSession(on_error=ConfigSessionError) +        else: +            self._vyconf_session = None +      def __del__(self):          try:              output = ( @@ -209,7 +218,10 @@ class ConfigSession(object):              value = []          else:              value = [value] -        self.__run_command([SET] + path + value) +        if self._vyconf_session is None: +            self.__run_command([SET] + path + value) +        else: +            self._vyconf_session.set(path + value)      def set_section(self, path: list, d: dict):          try: @@ -223,7 +235,10 @@ class ConfigSession(object):              value = []          else:              value = [value] -        self.__run_command([DELETE] + path + value) +        if self._vyconf_session is None: +            self.__run_command([DELETE] + path + value) +        else: +            self._vyconf_session.delete(path + value)      def load_section(self, path: list, d: dict):          try: @@ -261,20 +276,34 @@ class ConfigSession(object):          self.__run_command([COMMENT] + path + value)      def commit(self): -        out = self.__run_command([COMMIT]) +        if self._vyconf_session is None: +            out = self.__run_command([COMMIT]) +        else: +            out, _ = self._vyconf_session.commit() +          return out      def discard(self): -        self.__run_command([DISCARD]) +        if self._vyconf_session is None: +            self.__run_command([DISCARD]) +        else: +            out, _ = self._vyconf_session.discard()      def show_config(self, path, format='raw'): -        config_data = self.__run_command(SHOW_CONFIG + path) +        if self._vyconf_session is None: +            config_data = self.__run_command(SHOW_CONFIG + path) +        else: +            config_data, _ = self._vyconf_session.show_config()          if format == 'raw':              return config_data      def load_config(self, file_path): -        out = self.__run_command(LOAD_CONFIG + [file_path]) +        if self._vyconf_session is None: +            out = self.__run_command(LOAD_CONFIG + [file_path]) +        else: +            out, _ = self._vyconf_session.load_config(file=file_path) +          return out      def load_explicit(self, file_path): @@ -287,11 +316,21 @@ class ConfigSession(object):              raise ConfigSessionError(e) from e      def migrate_and_load_config(self, file_path): -        out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) +        if self._vyconf_session is None: +            out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) +        else: +            out, _ = self._vyconf_session.load_config(file=file_path, migrate=True) +          return out      def save_config(self, file_path): -        out = self.__run_command(SAVE_CONFIG + [file_path]) +        if self._vyconf_session is None: +            out = self.__run_command(SAVE_CONFIG + [file_path]) +        else: +            out, _ = self._vyconf_session.save_config( +                file=file_path, append_version=True +            ) +          return out      def install_image(self, url): diff --git a/python/vyos/kea.py b/python/vyos/kea.py index a2a35cf65..2b0cac7e6 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -43,6 +43,7 @@ kea4_options = {      'wpad_url': 'wpad-url',      'ipv6_only_preferred': 'v6-only-preferred',      'captive_portal': 'v4-captive-portal', +    'capwap_controller': 'capwap-ac-v4',  }  kea6_options = { @@ -55,6 +56,7 @@ kea6_options = {      'nisplus_server': 'nisp-servers',      'sntp_server': 'sntp-servers',      'captive_portal': 'v6-captive-portal', +    'capwap_controller': 'capwap-ac-v6',  }  kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket' diff --git a/python/vyos/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py index f34549309..b385f0951 100644 --- a/python/vyos/proto/vyconf_client.py +++ b/python/vyos/proto/vyconf_client.py @@ -52,7 +52,9 @@ def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvel  def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response:      # pylint: disable=no-member -    d = MessageToDict(msg, preserving_proto_field_name=True) +    d = MessageToDict( +        msg, preserving_proto_field_name=True, use_integers_for_enums=True +    )      response = vyconf_proto.Response(**d)      return response diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py new file mode 100644 index 000000000..506095625 --- /dev/null +++ b/python/vyos/vyconf_session.py @@ -0,0 +1,123 @@ +# Copyright 2025 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/>. +# +# + +import tempfile +import shutil +from functools import wraps +from typing import Type + +from vyos.proto import vyconf_client +from vyos.migrate import ConfigMigrate +from vyos.migrate import ConfigMigrateError +from vyos.component_version import append_system_version + + +def output(o): +    out = '' +    for res in (o.output, o.error, o.warning): +        if res is not None: +            out = out + res +    return out + + +class VyconfSession: +    def __init__(self, token: str = None, on_error: Type[Exception] = None): +        if token is None: +            out = vyconf_client.send_request('setup_session') +            self.__token = out.output +        else: +            self.__token = token + +        self.on_error = on_error + +    @staticmethod +    def raise_exception(f): +        @wraps(f) +        def wrapped(self, *args, **kwargs): +            if self.on_error is None: +                return f(self, *args, **kwargs) +            o, e = f(self, *args, **kwargs) +            if e: +                raise self.on_error(o) +            return o, e + +        return wrapped + +    @raise_exception +    def set(self, path: list[str]) -> tuple[str, int]: +        out = vyconf_client.send_request('set', token=self.__token, path=path) +        return output(out), out.status + +    @raise_exception +    def delete(self, path: list[str]) -> tuple[str, int]: +        out = vyconf_client.send_request('delete', token=self.__token, path=path) +        return output(out), out.status + +    @raise_exception +    def commit(self) -> tuple[str, int]: +        out = vyconf_client.send_request('commit', token=self.__token) +        return output(out), out.status + +    @raise_exception +    def discard(self) -> tuple[str, int]: +        out = vyconf_client.send_request('discard', token=self.__token) +        return output(out), out.status + +    def session_changed(self) -> bool: +        out = vyconf_client.send_request('session_changed', token=self.__token) +        return not bool(out.status) + +    @raise_exception +    def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]: +        # pylint: disable=consider-using-with +        if migrate: +            tmp = tempfile.NamedTemporaryFile() +            shutil.copy2(file, tmp.name) +            config_migrate = ConfigMigrate(tmp.name) +            try: +                config_migrate.run() +            except ConfigMigrateError as e: +                tmp.close() +                return repr(e), 1 +            file = tmp.name +        else: +            tmp = '' + +        out = vyconf_client.send_request('load', token=self.__token, location=file) +        if tmp: +            tmp.close() + +        return output(out), out.status + +    @raise_exception +    def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]: +        out = vyconf_client.send_request('save', token=self.__token, location=file) +        if append_version: +            append_system_version(file) +        return output(out), out.status + +    @raise_exception +    def show_config(self, path: list[str] = None) -> tuple[str, int]: +        if path is None: +            path = [] +        out = vyconf_client.send_request('show_config', token=self.__token, path=path) +        return output(out), out.status + +    def __del__(self): +        out = vyconf_client.send_request('teardown', token=self.__token) +        if out.status: +            print(f'Could not tear down session {self.__token}: {output(out)}') | 
