"""
Salt execution module for OpenWrt configuration via local ubus.
For devices with Python3 and ubusd running, managed via salt-ssh.
Calls ``ubus call`` directly via subprocess -- no proxy module needed.
The JSON output is identical to what the JSON-RPC and SSH adapters
return, so the state module works unchanged.
UCI metadata fields are returned with underscore prefixes to avoid
collision with UCI option names::
.type -> _type, .name -> _name, .anonymous -> _anonymous, .index -> _index
"""
import json
import logging
import subprocess
import salt.utils.path
from salt.exceptions import CommandExecutionError
from saltext.uci._internal import ubus_ops
log = logging.getLogger(__name__)
__virtualname__ = "uci"
__func_alias__ = {
"set_": "set",
"apply_": "apply",
}
def __virtual__():
if __opts__.get("proxy"):
return False, "Running as proxy minion -- use the proxy execution module instead"
if not salt.utils.path.which("ubus"):
return False, "ubus binary not found"
return __virtualname__
def _call(ubus_object, ubus_method, params=None):
"""Execute a local ubus call and return parsed JSON."""
args = ["ubus", "call", ubus_object, ubus_method]
if params is not None:
args.append(json.dumps(params))
result = subprocess.run(
args,
capture_output=True,
text=True,
check=False,
timeout=30,
)
if result.returncode != 0:
raise CommandExecutionError(
f"ubus call {ubus_object} {ubus_method} failed (rc={result.returncode}): {result.stderr.strip()}"
)
output = result.stdout.strip()
if not output:
return None
return json.loads(output)
# --- Read operations ---
[docs]
def get(config, section=None, option=None):
"""
Read UCI configuration via local ``ubus call uci get``.
Returns the full config, a single section, or a single option value.
UCI metadata fields (.type, .name, .anonymous, .index) are returned
with underscore prefixes (_type, _name, _anonymous, _index).
CLI Example:
.. code-block:: bash
salt device uci.get network
salt device uci.get network lan
salt device uci.get network lan proto
"""
return ubus_ops.get(_call, config, section, option)
[docs]
def configs():
"""
List available UCI configuration packages.
CLI Example:
.. code-block:: bash
salt device uci.configs
"""
return ubus_ops.configs(_call)
[docs]
def changes(config):
"""
Show uncommitted changes for a UCI package.
CLI Example:
.. code-block:: bash
salt device uci.changes network
"""
return ubus_ops.changes(_call, config)
[docs]
def config_export(config, format="json"): # pylint: disable=redefined-builtin
"""
Export live UCI config as a grouped sections dict.
Transforms the device config into the format consumed by
``uci.managed()``. Anonymous sections are emitted as
singleton (``_<type>``) or multi-instance (``_<type>s`` with
``_match`` and ``_items``).
Use ``format=pillar`` to redact sensitive values with Jinja2
pillar references, or ``format=json`` (default) for plaintext.
CLI Example:
.. code-block:: bash
salt device uci.config_export network
salt device uci.config_export network format=pillar
"""
return ubus_ops.config_export(_call, config, format=format)
[docs]
def config_export_all(format="json"): # pylint: disable=redefined-builtin
"""
Export all UCI config packages as a grouped dict.
CLI Example:
.. code-block:: bash
salt device uci.config_export_all
salt device uci.config_export_all format=pillar
"""
return ubus_ops.config_export_all(_call, format=format)
[docs]
def config_diff(config, sections):
"""
Compare live UCI config against declared sections and return drift.
Read-only -- no writes are issued. Returns a categorized dict with
``changed``, ``new``, ``removed``, ``reordered``, and ``summary``.
CLI Example:
.. code-block:: bash
salt device uci.config_diff network sections='{"lan": {"ipaddr": "10.0.0.2"}}'
"""
return ubus_ops.config_diff(_call, config, sections)
# --- Write operations ---
[docs]
def set_(config, section, values):
"""
Set UCI option values on an existing section.
CLI Example:
.. code-block:: bash
salt device uci.set network lan '{"proto": "static"}'
"""
return ubus_ops.set_(_call, config, section, values)
[docs]
def add(config, type_, name=None, values=None):
"""
Add a new UCI section.
CLI Example:
.. code-block:: bash
salt device uci.add network interface name=wan2
"""
return ubus_ops.add(_call, config, type_, name, values)
[docs]
def delete(config, section, option=None):
"""
Delete a UCI section or option.
CLI Example:
.. code-block:: bash
salt device uci.delete network wan2
salt device uci.delete network lan dns
"""
return ubus_ops.delete(_call, config, section, option)
# --- Apply operations ---
[docs]
def apply_(rollback=90): # pylint: disable=redefined-outer-name
"""
Commit and apply UCI changes with rollback safety.
CLI Example:
.. code-block:: bash
salt device uci.apply
salt device uci.apply rollback=120
"""
return ubus_ops.apply_(_call, rollback)
[docs]
def confirm():
"""
Confirm a pending apply, locking in the changes.
CLI Example:
.. code-block:: bash
salt device uci.confirm
"""
return ubus_ops.confirm(_call)
[docs]
def rollback():
"""
Manually trigger a rollback of the last apply.
CLI Example:
.. code-block:: bash
salt device uci.rollback
"""
return ubus_ops.rollback(_call)
[docs]
def revert(config):
"""
Discard staged (uncommitted) changes for a UCI package.
CLI Example:
.. code-block:: bash
salt device uci.revert network
"""
return ubus_ops.revert(_call, config)
[docs]
def commit(config):
"""
Commit staged changes to /etc/config without reloading daemons.
CLI Example:
.. code-block:: bash
salt device uci.commit network
"""
return ubus_ops.commit(_call, config)
[docs]
def state(config, section=None):
"""
Return runtime-merged UCI state (defaults + config + overrides).
CLI Example:
.. code-block:: bash
salt device uci.state network
salt device uci.state network lan
"""
return ubus_ops.state(_call, config, section)
# --- System info ---
[docs]
def system_board():
"""
Return system board information.
CLI Example:
.. code-block:: bash
salt device uci.system_board
"""
return ubus_ops.system_board(_call)
[docs]
def system_info():
"""
Return system info (memory, uptime, load).
CLI Example:
.. code-block:: bash
salt device uci.system_info
"""
return ubus_ops.system_info(_call)
[docs]
def network_dump():
"""
Return network interface state.
CLI Example:
.. code-block:: bash
salt device uci.network_dump
"""
return ubus_ops.network_dump(_call)
# --- Service info ---
[docs]
def service_list(verbose=False):
"""
Return procd service list.
CLI Example:
.. code-block:: bash
salt device uci.service_list
salt device uci.service_list verbose=True
"""
return ubus_ops.service_list(_call, verbose)