Page MenuHomeVyOS Platform

Operational command handling daemon
Open, NormalPublic

Description

The idea to move operational command handling to a daemon has been floating around for a few years already, but we never made a serious effort trying to design it. With HTTP API and the controller project, machine-friendly op mode becomes a requirement, so it's time to get to it.

Right now, operational commands are controlled by /opt/vyatta/bin/vyatta-op-cmd-wrapper, which uses command "templates" in /opt/vyatta/share/vyatta-op/templates. Its main assumption is that every op mode command maps to a shell command. Every time a user run an op mode command, that script find which exact command it maps to and executes it in the system shell (bash).

That leads to obvious problems:

  • Performance overhead and latency due to interpreter startup cost (Python runs reasonably fast, but it's slow to start).
  • vyatta-op-cmd-wrapper is a shell script that makes extensive use of bash's programmable completion, and it's hard to maintain and extend.
  • There is no unified way to request machine-readable output from scripts, or adds any other flags (e.g. level of detail).

Since we already produce /opt/vyatta/share/vyatta-op/templates from XML definitions, it's relatively easy to finally get rid of the original shell version and replace it with a new implementation. Much easier than with conf mode, since op mode is completely stateless and doesn't involve any advanced algorithms: it just maps abstract commands to real executable code.

Op mode daemon operation

The op mode daemon reads commands (lists of tokens) and routes them to specific Python modules. Op mode Python modules will provide standardized functions that the daemon can call, as per T2719.

Instead of having a <command> element in every node, a whole sub-tree can be owned by a single module. Sub-commands will be routed to the sub-tree owner module and "extra" tokens will be used as arguments to module functions.

There's already enough information in the XML definitions to tell if something is a fixed keyword or a variably-named parameter. Fixed tokens are in <node> and <leafNode> elements, while variable parameters are made with <tagNode>. If we add a new mandatory parameter to <tagNode> that will give the "tag" a name, we can automatically translate them to named arguments for get_raw_data/get_formatted_output. Fixed parameters can be passed as lists of keywords, or as dicts with boolean values.

If we group op mode commands so that fixed modifier keywords always come after tag nodes, that will further simplify the implementation,

Consider the following op mode command tree:

show
    interfaces [owner=show_interfaces.py]
        ethernet [tag: interface]
          physical [owner=show_eth.py]
        bridge     [tag: interface]

First, suppose the user requests show interfaces ethernet eth0 physical. Since the interfaces/ethernet/physical node has owner=show_eth.py, that module is used. Since ethernet is a tag node with tag="interface", vyos-opd will call show_eth.get_formatted_output(interface="eth0").

Now, suppose the user runs show interfaces ethernet eth0. The ethernet node doesn't have an owner, so vyos-opd goes up the tree and finds the nearest upper-level node that has an owner: that's interfaces with owner=show_interfaces.py. The daemon then calls show_interfaces.get_formatted_output(interface="eth0") because it again knows that ethernet is a tag node and eth0 is a parameter named interface.

Keyword arguments and completion

If we group commands so that fixed keywords are not interspersed with tag nodes, we can pass them as keyword arguments (or a part of a boolean dict).

For example, suppose the user runs show interfaces ethernet eth0 detail. The daemon knows that show interfaces ethernet eth0 should be mapped to show_interfaces.get_formatted_output(interface="eth0"). The leftover of the command is detail. The daemon can pass it as show_interfaces.get_formatted_output(interface="eth0", ["detail"]).

If command completion is delegated to the op mode daemon client, it also opens up a way to introduce well-known keywords. For example, all commands may support a "level of detail" argument that can be e.g. brief/normal/detail/debug. Some commands may not implement it and ignore it, of course. Right now detail (where it exists) is a sub-node, so moving it to well-known arguments handled by the daemon itself will also simplify the command definitions.

Likewise, json can be a common argument that forces the daemon to call get_raw_data and return its JSON instead of returning human-readable output. Commands that don't support it will show an error.

Completion operation

There are two possible approaches. One way is to auto-generate bash completion files from command definitions. I think it's a bad way because it doesn't compose with well-known arguments: it will produce a lot of sub-trees for different combinations of "detail/brief/debug" and "json", for example (that already would give eight combinations, and with more options it will be a real combinatorial explosion).

The second way is to delegate it to the daemon itself and make bash call its client with a complete sub-command. Completion outputs can be memoized, which may make it faster than the current bash completion.
Prerequisites

  • Op mode commands must not run any Linux commands directly. The <command> tag must be replaced by something like <owner> which points at a Python module.
  • <tagNode> elements must come with a name attribute for the parameter, e.g. <tagNode tag="interface">.
  • All op mode Python modules must provide a get_formatted_output function that produces human-readable output.
  • Op mode Python modules should provide a get_raw_data function that returns a dict that can be encoded in JSON.
  • Op mode commands must be grouped so that every sub-tree have a single owner.

Details

Difficulty level
Unknown (require assessment)
Version
-
Why the issue appeared?
Will be filled on close
Is it a breaking change?
Behavior change
Issue type
Internal change (not visible to end users)