Skip to content
This repository was archived by the owner on May 27, 2026. It is now read-only.

Commit dc72756

Browse files
committed
op-mode: T7583: add a new operational command runner
1 parent 5136180 commit dc72756

6 files changed

Lines changed: 323 additions & 0 deletions

File tree

debian/changelog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
vyos-utils (0.0.4) unstable; urgency=medium
2+
3+
* Add an operational command runner.
4+
5+
-- Daniil Baturin <daniil@vyos.io> Thu, 07 Aug 2025 12:39:14 +0000
6+
17
vyos-utils (0.0.3) unstable; urgency=medium
28

39
* Quote values properly (T1901).

debian/rules

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ override_dh_auto_build:
1313
override_dh_auto_install:
1414
mkdir -p $(DIR)/usr/libexec/vyos/validators
1515
mkdir -p $(DIR)/usr/libexec/vyos/completion
16+
mkdir -p $(DIR)/usr/bin
1617
cp _build/install/default/bin/numeric $(DIR)/usr/libexec/vyos/validators
1718
cp _build/install/default/bin/validate-value $(DIR)/usr/libexec/vyos/
1819
cp _build/install/default/bin/file-path $(DIR)/usr/libexec/vyos/validators
1920
cp _build/install/default/bin/url $(DIR)/usr/libexec/vyos/validators
2021
cp _build/install/default/bin/list_interfaces $(DIR)/usr/libexec/vyos/completion
22+
cp _build/install/default/bin/vyos-op-run $(DIR)/usr/bin/
2123

2224
override_dh_auto_test:
2325
echo "No tests yet"

debian/vyos-utils.install

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ usr/libexec/vyos/validators/numeric
33
usr/libexec/vyos/validators/file-path
44
usr/libexec/vyos/validators/url
55
usr/libexec/vyos/completion/list_interfaces
6+
usr/bin/vyos-op-run

debian/vyos-utils.postinst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
3+
# Set the SUID bit on the runner
4+
# so that it can have the prigilege to run commands as root.
5+
# Permissions will be enforced at the VyOS command level.
6+
chmod u+s /usr/bin/vyos-op-run

src/dune

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,16 @@
3232
(foreign_stubs
3333
(language c)
3434
(names iface)))
35+
36+
(executable
37+
(name vyos_op_run)
38+
(public_name vyos-op-run)
39+
(modules vyos_op_run)
40+
(libraries
41+
logs
42+
logs.fmt
43+
fmt.tty
44+
yojson
45+
mustache
46+
pcre2
47+
unix))

src/vyos_op_run.ml

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
(*
2+
* vyos-op-run: the wrapper for executing operational mode commands.
3+
*
4+
* Copyright VyOS maintainers and contributors <maintainers@vyos.io>
5+
*
6+
* This program is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 2 or later as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*)
18+
19+
(* Global constants *)
20+
(* let op_def_file = "/usr/share/vyos/op_cache.json" *)
21+
let op_def_file = "/tmp/op_cache.json"
22+
23+
(* Execution options *)
24+
type options = {
25+
(* The option not to actually run the command, just print it *)
26+
dry_run: bool;
27+
28+
(* Enable debug output *)
29+
debug: bool;
30+
31+
(* The original VyOS command,
32+
like "show interfaces ethernet",
33+
for debugging and for substitutions of $@/$*
34+
*)
35+
vyos_command: string;
36+
}
37+
38+
let default_options = {
39+
dry_run = false;
40+
debug = false;
41+
vyos_command = "<VyOS command is undefined>";
42+
}
43+
44+
(* Exceptions and helpers *)
45+
exception Invalid_command of string
46+
let invalid_command msg = raise (Invalid_command msg)
47+
48+
exception Internal_error of string
49+
let internal_error msg = raise (Internal_error msg)
50+
51+
exception Command_error of string
52+
let command_error msg = raise (Command_error msg)
53+
54+
exception Incomplete_command
55+
56+
(* Logging setup routines *)
57+
let get_color_style () =
58+
let no_color = Sys.getenv_opt "NO_COLOR" |> Option.is_some in
59+
(* Logs always go to stderr, so we don't check if stdout is a TTY. *)
60+
let interactive = Unix.isatty (Unix.descr_of_out_channel stderr) in
61+
if interactive && (not no_color) then `Ansi_tty else `None
62+
63+
let setup_logging debug =
64+
let level =
65+
if debug then Logs.Debug
66+
else Logs.Warning
67+
in
68+
let style = get_color_style () in
69+
Logs.set_level (Some level);
70+
Fmt_tty.setup_std_outputs ~style_renderer:style ();
71+
Logs.set_reporter @@ Logs.format_reporter ();
72+
(* Enable exception tracing if debug=true,
73+
by default it's disabled in the OCaml runtime *)
74+
if debug then Printexc.record_backtrace true
75+
76+
(* JSON data helpers *)
77+
let get_string_field name obj =
78+
let open Yojson.Safe.Util in
79+
member name obj |> to_string
80+
81+
let read_command_definitions () =
82+
let ic = open_in op_def_file in
83+
let data = Yojson.Safe.from_channel ic in
84+
let () = close_in ic in
85+
data
86+
87+
let find_child_node op_node word =
88+
let open Yojson.Safe.Util in
89+
let res = member word op_node in
90+
match res with
91+
| (`Assoc _) as d -> Some d
92+
| `Null -> None
93+
| _ ->
94+
Printf.ksprintf internal_error {|Child node "%s" is not an object!|} word
95+
96+
let get_node_data op_node =
97+
let open Yojson.Safe.Util in
98+
let res = member "__node_data" op_node in
99+
match res with
100+
| (`Assoc _) as d -> d
101+
| `Null ->
102+
Printf.ksprintf internal_error "Op node has no data!\n"
103+
| _ ->
104+
Printf.ksprintf internal_error "Op node data is not an object!"
105+
106+
let get_path node_data =
107+
let open Yojson.Safe.Util in
108+
member "path" node_data |> convert_each to_string
109+
110+
let get_node_type node_data =
111+
let open Yojson.Safe.Util in
112+
let res = member "node_type" node_data in
113+
match res with
114+
| `String _type -> _type
115+
| `Null ->
116+
Printf.ksprintf internal_error "Op node has no type!"
117+
| _ ->
118+
Printf.ksprintf internal_error "Op node data is not a string!"
119+
120+
let get_command_opt ?(field_name="command") node_data =
121+
let open Yojson.Safe.Util in
122+
let res = member field_name node_data in
123+
match res with
124+
| `String cmd -> Some cmd
125+
| `Null -> None
126+
| _ -> Printf.ksprintf internal_error "command must be a string"
127+
128+
let get_command ?(field_name="command") node_data =
129+
let res = get_command_opt ~field_name:field_name node_data in
130+
match res with
131+
| Some cmd -> cmd
132+
| None -> Printf.ksprintf internal_error "node is expected to have a command"
133+
134+
let get_virtual_tag_node node =
135+
let open Yojson.Safe.Util in
136+
let res = member "__virtual_tag" node in
137+
match res with
138+
| `Null -> None
139+
| _ -> Some res
140+
141+
(* Command rendering and execution *)
142+
let render_command opts env command_tmpl =
143+
let () = Logs.debug @@ fun m -> m "Command template: %s" command_tmpl in
144+
let command_tmpl = (Mustache.of_string command_tmpl) in
145+
let command = Mustache.render command_tmpl (`O env) in
146+
let vyos_command = opts.vyos_command in
147+
Pcre2.replace ~pat:{|\$[@*]|} ~templ:vyos_command command
148+
149+
let run_command opts env command_tmpl =
150+
let cmd = render_command opts env command_tmpl in
151+
if opts.dry_run then Printf.printf "%s\n%!" cmd else
152+
let () = Logs.debug @@ fun m -> m "Command to be executed %s" cmd in
153+
let res = Unix.system cmd in
154+
match res with
155+
| Unix.WEXITED 0 -> ()
156+
| _ -> Printf.ksprintf command_error "Execution of command '%s' failed" cmd
157+
158+
(* Command lookup *)
159+
let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
160+
match cmd_words with
161+
| w :: ws ->
162+
let () = Logs.debug @@ fun m -> m "Looking up node '%s'" w in
163+
let res = find_child_node node w in
164+
begin match res with
165+
| Some child_node ->
166+
(* It's a normal, fixed command word *)
167+
find_command opts ~env:env ~parent:w child_node ws
168+
| None ->
169+
(* It's either an argument of a tag node
170+
or an incorrect command word *)
171+
let node_data = get_node_data node in
172+
let node_type = get_string_field "node_type" node_data in
173+
let virtual_tag_node = get_virtual_tag_node node in
174+
match node_type, virtual_tag_node with
175+
| "tagNode", None ->
176+
(* It's a simple tag node *)
177+
let env = (Printf.sprintf "%s-tag_value" parent, `String w) :: env in
178+
begin match ws with
179+
| [] ->
180+
let command = get_command node_data in
181+
run_command opts env command
182+
| _ as ws ->
183+
find_command opts ~env:env ~parent:w node ws
184+
end
185+
| "node", Some vtn ->
186+
(* It's a command that can be used either by itself or with an argument. *)
187+
let env = (Printf.sprintf "%s-tag_value" parent, `String w) :: env in
188+
begin match ws with
189+
| [] ->
190+
let vtn_data = get_node_data vtn in
191+
let command = get_command vtn_data in
192+
run_command opts env command
193+
| _ ->
194+
(* In the case of a virtual tag node, we take the parent (for variable substitution purposes)
195+
from the upper level.
196+
*)
197+
find_command opts ~env:env ~parent:parent vtn ws
198+
end
199+
| "node", None | "leafNode", None ->
200+
let path = get_path node_data in
201+
Printf.ksprintf invalid_command {|"%s" is not a valid argument for command [%s]|}
202+
w (String.concat " " path)
203+
| _, _ ->
204+
Printf.ksprintf internal_error
205+
{|Node with type "%s" must not have a <virtualTagNode> child|}
206+
node_type
207+
end
208+
| _ ->
209+
let node_data = get_node_data node in
210+
let node_type = get_node_type node_data in
211+
let command =
212+
begin match node_type with
213+
| "node" | "leafNode" ->
214+
get_command_opt node_data
215+
| "tagNode" ->
216+
(* If it's a tag node but there's no argument,
217+
we need to check if that tag node has standalone behavior attached to it.
218+
*)
219+
get_command_opt ~field_name:"standalone_command" node_data
220+
| "virtualTagNode" ->
221+
None
222+
| _ -> Printf.ksprintf internal_error {|Invalid node type "%s"|} node_type
223+
end
224+
in
225+
begin match command with
226+
| Some command ->
227+
run_command opts env command
228+
| None ->
229+
raise Incomplete_command
230+
end
231+
232+
(* Command line argument parsing *)
233+
let usage_msg = Printf.sprintf {|Usage: %s [OPTIONS] <command>
234+
235+
%s is the VyOS operational command wrapper.
236+
It is used by the CLI and can be used
237+
for running operational commands from scripts.
238+
239+
Options:
240+
|} Sys.argv.(0) Sys.argv.(0)
241+
242+
let get_args () =
243+
let opts = ref default_options in
244+
let args = ref [] in
245+
let add_positional_arg arg =
246+
args := arg :: !args
247+
in
248+
let arg_spec = Arg.align [
249+
("--dry-run",
250+
Arg.Unit (fun () -> opts := {!opts with dry_run=true}),
251+
"Show the command instead of executing it");
252+
("--debug",
253+
Arg.Unit (fun () -> opts := {!opts with debug=true}),
254+
"Enable debug output");
255+
]
256+
in
257+
let () = Arg.parse arg_spec add_positional_arg usage_msg in
258+
let args = List.rev !args in
259+
({!opts with vyos_command=(String.concat " " args)}, args)
260+
261+
let () =
262+
let debug =
263+
(* For simplicity, we check for the existence
264+
of the VYOS_DEBUG environment variable,
265+
rather than for specific values.
266+
*)
267+
match Unix.getenv "VYOS_DEBUG" with
268+
| _ -> true
269+
| exception Not_found -> false
270+
in
271+
let options, args = get_args () in
272+
(* If debug is not enabled by the environment variable,
273+
take it from command line options --
274+
it may be enabled there.
275+
*)
276+
let () = if debug then print_endline "Debug is enabled by the env var" in
277+
let debug = if debug then true else options.debug in
278+
let () = setup_logging debug in
279+
let op_defs = read_command_definitions () in
280+
let () = Unix.setuid 0 in
281+
try
282+
find_command options ~env:[] ~parent:"" op_defs args
283+
with
284+
| Invalid_command msg ->
285+
Printf.fprintf stderr "Invalid command [%s]: %s" options.vyos_command msg;
286+
exit 1
287+
| Command_error msg ->
288+
Printf.fprintf stderr "%s" msg;
289+
| Incomplete_command ->
290+
Printf.fprintf stderr "Incomplete command: %s" options.vyos_command;
291+
exit 2
292+
| Internal_error msg ->
293+
Printf.fprintf stderr "Internal error: %s" msg;
294+
exit 255
295+

0 commit comments

Comments
 (0)