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

Commit 9285b34

Browse files
authored
Merge pull request #40 from dmbaturin/T7583-op-mode-runner
op-mode: T7583: add a new operational command runner
2 parents d93ec62 + ad5b369 commit 9285b34

6 files changed

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

0 commit comments

Comments
 (0)