# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from napalm.base.exceptions import ( CommandErrorException, MergeConfigException, ReplaceConfigException, ) from napalm_device_types import ConfigLifecycleMixin class OpenWrtConfigMixin(ConfigLifecycleMixin): """Mixin providing configuration-management NAPALM methods for OpenWrt. Reuses the standard lifecycle (load/compare/discard) from :class:`ConfigLifecycleMixin` and adds OpenWrt-specific ``commit_config`` and ``rollback``. """ _comment_chars = ("#",) def _get_running_config(self) -> str: return self._send_command("uci export") def commit_config(self, message: str = "", revert_in: int | None = None) -> None: """Apply the staged candidate configuration and commit it. **Merge mode**: each UCI command line is sent to the device shell, then ``uci commit`` is called to persist the changes. **Replace mode**: the candidate is piped through ``uci import`` and then ``uci commit`` is called for every affected package. :raises MergeConfigException: if no candidate is staged or if commands are rejected. :raises ReplaceConfigException: same, for replace candidates. """ if self._candidate_config is None: raise MergeConfigException("No candidate configuration is staged.") ex_cls = ReplaceConfigException if self._candidate_mode == "replace" else MergeConfigException self._backup_config = self._send_command("uci export") errors: list[str] = [] try: if self._candidate_mode == "merge": for line in self._candidate_config.splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#"): continue out = self._send_command(stripped) if out and ("uci: " in out.lower() or "error" in out.lower()): errors.append(f" {stripped!r}: {out}") self._send_command("uci commit") else: escaped = self._candidate_config.replace("'", "'\\''") self._send_command(f"printf '%s' '{escaped}' > /tmp/napalm_candidate.uci") out = self._send_command("uci import < /tmp/napalm_candidate.uci && uci commit") self._send_command("rm -f /tmp/napalm_candidate.uci") if out and "error" in out.lower(): errors.append(out) except Exception as exc: raise ex_cls(str(exc)) from exc if errors: raise ex_cls("The following commands were rejected:\n" + "\n".join(errors)) self._candidate_config = None self._candidate_mode = None def rollback(self) -> None: """Restore the UCI configuration to the state before the last :meth:`commit_config`. Pipes the saved backup through ``uci import`` and then commits. :raises CommandErrorException: if no backup is available. """ if self._backup_config is None: raise CommandErrorException( "No backup configuration available – commit_config has not been called in this session." ) escaped = self._backup_config.replace("'", "'\\''") self._send_command(f"printf '%s' '{escaped}' > /tmp/napalm_rollback.uci") self._send_command("uci import < /tmp/napalm_rollback.uci && uci commit") self._send_command("rm -f /tmp/napalm_rollback.uci") self._backup_config = None