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

Commit cbf1553

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

3 files changed

Lines changed: 291 additions & 0 deletions

File tree

debian/rules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ override_dh_auto_install:
1818
cp _build/install/default/bin/file-path $(DIR)/usr/libexec/vyos/validators
1919
cp _build/install/default/bin/url $(DIR)/usr/libexec/vyos/validators
2020
cp _build/install/default/bin/list_interfaces $(DIR)/usr/libexec/vyos/completion
21+
cp _build/install/default/bin/vyos-op-run $(DIR)/usr/bin/
22+
chown root:root $(DIR)/usr/bin/vyos-op-run
23+
chmod +s $(DIR)/usr/bin/vyos-op-run
2124

2225
override_dh_auto_test:
2326
echo "No tests yet"

src/dune

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,15 @@
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+
unix))

src/vyos_op_run.ml

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

0 commit comments

Comments
 (0)