diff options
| author | Daniil Baturin <daniil@baturin.org> | 2025-08-07 13:15:59 +0100 |
|---|---|---|
| committer | Daniil Baturin <daniil@baturin.org> | 2025-08-07 13:56:50 +0100 |
| commit | ad5b369a63ed07c6d92b453f4f73bfe7993ff053 (patch) | |
| tree | 9c6e6032a039c13fe78ef00cd308611d1c0fab74 | |
| parent | 5136180e78d9794e100ac8c8ccc9d6716670be75 (diff) | |
| download | vyos-1x-ad5b369a63ed07c6d92b453f4f73bfe7993ff053.tar.gz vyos-1x-ad5b369a63ed07c6d92b453f4f73bfe7993ff053.zip | |
op-mode: T7583: add a new operational command runner
| -rw-r--r-- | debian/changelog | 6 | ||||
| -rwxr-xr-x | debian/rules | 2 | ||||
| -rw-r--r-- | debian/vyos-utils.install | 1 | ||||
| -rw-r--r-- | debian/vyos-utils.postinst | 6 | ||||
| -rw-r--r-- | src/dune | 13 | ||||
| -rw-r--r-- | src/vyos_op_run.ml | 294 |
6 files changed, 322 insertions, 0 deletions
diff --git a/debian/changelog b/debian/changelog index e0ac2fa3f..0db92790a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vyos-utils (0.0.4) unstable; urgency=medium + + * Add an operational command runner. + + -- Daniil Baturin <daniil@vyos.io> Thu, 07 Aug 2025 12:39:14 +0000 + vyos-utils (0.0.3) unstable; urgency=medium * Quote values properly (T1901). diff --git a/debian/rules b/debian/rules index 451eef1ba..ea313abfc 100755 --- a/debian/rules +++ b/debian/rules @@ -13,11 +13,13 @@ override_dh_auto_build: override_dh_auto_install: mkdir -p $(DIR)/usr/libexec/vyos/validators mkdir -p $(DIR)/usr/libexec/vyos/completion + mkdir -p $(DIR)/usr/bin cp _build/install/default/bin/numeric $(DIR)/usr/libexec/vyos/validators cp _build/install/default/bin/validate-value $(DIR)/usr/libexec/vyos/ cp _build/install/default/bin/file-path $(DIR)/usr/libexec/vyos/validators cp _build/install/default/bin/url $(DIR)/usr/libexec/vyos/validators cp _build/install/default/bin/list_interfaces $(DIR)/usr/libexec/vyos/completion + cp _build/install/default/bin/vyos-op-run $(DIR)/usr/bin/ override_dh_auto_test: echo "No tests yet" diff --git a/debian/vyos-utils.install b/debian/vyos-utils.install index 9cc90a1b3..55eefd4e7 100644 --- a/debian/vyos-utils.install +++ b/debian/vyos-utils.install @@ -3,3 +3,4 @@ usr/libexec/vyos/validators/numeric usr/libexec/vyos/validators/file-path usr/libexec/vyos/validators/url usr/libexec/vyos/completion/list_interfaces +usr/bin/vyos-op-run diff --git a/debian/vyos-utils.postinst b/debian/vyos-utils.postinst new file mode 100644 index 000000000..decc38f60 --- /dev/null +++ b/debian/vyos-utils.postinst @@ -0,0 +1,6 @@ +#!/bin/sh + +# Set the SUID bit on the runner +# so that it can have the prigilege to run commands as root. +# Permissions will be enforced at the VyOS command level. +chmod u+s /usr/bin/vyos-op-run @@ -32,3 +32,16 @@ (foreign_stubs (language c) (names iface))) + +(executable + (name vyos_op_run) + (public_name vyos-op-run) + (modules vyos_op_run) + (libraries + logs + logs.fmt + fmt.tty + yojson + mustache + pcre2 + unix)) diff --git a/src/vyos_op_run.ml b/src/vyos_op_run.ml new file mode 100644 index 000000000..0a8f35134 --- /dev/null +++ b/src/vyos_op_run.ml @@ -0,0 +1,294 @@ +(* + * vyos-op-run: the wrapper for executing operational mode commands. + * + * Copyright VyOS maintainers and contributors <maintainers@vyos.io> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or later as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + *) + +(* Global constants *) +let op_def_file = "/usr/share/vyos/op_cache.json" + +(* Execution options *) +type options = { + (* The option not to actually run the command, just print it *) + dry_run: bool; + + (* Enable debug output *) + debug: bool; + + (* The original VyOS command, + like "show interfaces ethernet", + for debugging and for substitutions of $@/$* + *) + vyos_command: string; +} + +let default_options = { + dry_run = false; + debug = false; + vyos_command = "<VyOS command is undefined>"; +} + +(* Exceptions and helpers *) +exception Invalid_command of string +let invalid_command msg = raise (Invalid_command msg) + +exception Internal_error of string +let internal_error msg = raise (Internal_error msg) + +exception Command_error of string +let command_error msg = raise (Command_error msg) + +exception Incomplete_command + +(* Logging setup routines *) +let get_color_style () = + let no_color = Sys.getenv_opt "NO_COLOR" |> Option.is_some in + (* Logs always go to stderr, so we don't check if stdout is a TTY. *) + let interactive = Unix.isatty (Unix.descr_of_out_channel stderr) in + if interactive && (not no_color) then `Ansi_tty else `None + +let setup_logging debug = + let level = + if debug then Logs.Debug + else Logs.Warning + in + let style = get_color_style () in + Logs.set_level (Some level); + Fmt_tty.setup_std_outputs ~style_renderer:style (); + Logs.set_reporter @@ Logs.format_reporter (); + (* Enable exception tracing if debug=true, + by default it's disabled in the OCaml runtime *) + if debug then Printexc.record_backtrace true + +(* JSON data helpers *) +let get_string_field name obj = + let open Yojson.Safe.Util in + member name obj |> to_string + +let read_command_definitions () = + let ic = open_in op_def_file in + let data = Yojson.Safe.from_channel ic in + let () = close_in ic in + data + +let find_child_node op_node word = + let open Yojson.Safe.Util in + let res = member word op_node in + match res with + | (`Assoc _) as d -> Some d + | `Null -> None + | _ -> + Printf.ksprintf internal_error {|Child node "%s" is not an object!|} word + +let get_node_data op_node = + let open Yojson.Safe.Util in + let res = member "__node_data" op_node in + match res with + | (`Assoc _) as d -> d + | `Null -> + Printf.ksprintf internal_error "Op node has no data!\n" + | _ -> + Printf.ksprintf internal_error "Op node data is not an object!" + +let get_path node_data = + let open Yojson.Safe.Util in + member "path" node_data |> convert_each to_string + +let get_node_type node_data = + let open Yojson.Safe.Util in + let res = member "node_type" node_data in + match res with + | `String _type -> _type + | `Null -> + Printf.ksprintf internal_error "Op node has no type!" + | _ -> + Printf.ksprintf internal_error "Op node data is not a string!" + +let get_command_opt ?(field_name="command") node_data = + let open Yojson.Safe.Util in + let res = member field_name node_data in + match res with + | `String cmd -> Some cmd + | `Null -> None + | _ -> Printf.ksprintf internal_error "command must be a string" + +let get_command ?(field_name="command") node_data = + let res = get_command_opt ~field_name:field_name node_data in + match res with + | Some cmd -> cmd + | None -> Printf.ksprintf internal_error "node is expected to have a command" + +let get_virtual_tag_node node = + let open Yojson.Safe.Util in + let res = member "__virtual_tag" node in + match res with + | `Null -> None + | _ -> Some res + +(* Command rendering and execution *) +let render_command opts env command_tmpl = + let () = Logs.debug @@ fun m -> m "Command template: %s" command_tmpl in + let command_tmpl = (Mustache.of_string command_tmpl) in + let command = Mustache.render command_tmpl (`O env) in + let vyos_command = opts.vyos_command in + Pcre2.replace ~pat:{|\$[@*]|} ~templ:vyos_command command + +let run_command opts env command_tmpl = + let cmd = render_command opts env command_tmpl in + if opts.dry_run then Printf.printf "%s\n%!" cmd else + let () = Logs.debug @@ fun m -> m "Command to be executed %s" cmd in + let res = Unix.system cmd in + match res with + | Unix.WEXITED 0 -> () + | _ -> Printf.ksprintf command_error "Execution of command '%s' failed" cmd + +(* Command lookup *) +let rec find_command opts ?(env=[]) ?(parent="") node cmd_words = + match cmd_words with + | w :: ws -> + let () = Logs.debug @@ fun m -> m "Looking up node '%s'" w in + let res = find_child_node node w in + begin match res with + | Some child_node -> + (* It's a normal, fixed command word *) + find_command opts ~env:env ~parent:w child_node ws + | None -> + (* It's either an argument of a tag node + or an incorrect command word *) + let node_data = get_node_data node in + let node_type = get_string_field "node_type" node_data in + let virtual_tag_node = get_virtual_tag_node node in + match node_type, virtual_tag_node with + | "tagNode", None -> + (* It's a simple tag node *) + let env = (Printf.sprintf "%s-tag_value" parent, `String w) :: env in + begin match ws with + | [] -> + let command = get_command node_data in + run_command opts env command + | _ as ws -> + find_command opts ~env:env ~parent:w node ws + end + | "node", Some vtn -> + (* It's a command that can be used either by itself or with an argument. *) + let env = (Printf.sprintf "%s-tag_value" parent, `String w) :: env in + begin match ws with + | [] -> + let vtn_data = get_node_data vtn in + let command = get_command vtn_data in + run_command opts env command + | _ -> + (* In the case of a virtual tag node, we take the parent (for variable substitution purposes) + from the upper level. + *) + find_command opts ~env:env ~parent:parent vtn ws + end + | "node", None | "leafNode", None -> + let path = get_path node_data in + Printf.ksprintf invalid_command {|"%s" is not a valid argument for command [%s]|} + w (String.concat " " path) + | _, _ -> + Printf.ksprintf internal_error + {|Node with type "%s" must not have a <virtualTagNode> child|} + node_type + end + | _ -> + let node_data = get_node_data node in + let node_type = get_node_type node_data in + let command = + begin match node_type with + | "node" | "leafNode" -> + get_command_opt node_data + | "tagNode" -> + (* If it's a tag node but there's no argument, + we need to check if that tag node has standalone behavior attached to it. + *) + get_command_opt ~field_name:"standalone_command" node_data + | "virtualTagNode" -> + None + | _ -> Printf.ksprintf internal_error {|Invalid node type "%s"|} node_type + end + in + begin match command with + | Some command -> + run_command opts env command + | None -> + raise Incomplete_command + end + +(* Command line argument parsing *) +let usage_msg = Printf.sprintf {|Usage: %s [OPTIONS] <command> + +%s is the VyOS operational command wrapper. +It is used by the CLI and can be used +for running operational commands from scripts. + +Options: +|} Sys.argv.(0) Sys.argv.(0) + +let get_args () = + let opts = ref default_options in + let args = ref [] in + let add_positional_arg arg = + args := arg :: !args + in + let arg_spec = Arg.align [ + ("--dry-run", + Arg.Unit (fun () -> opts := {!opts with dry_run=true}), + "Show the command instead of executing it"); + ("--debug", + Arg.Unit (fun () -> opts := {!opts with debug=true}), + "Enable debug output"); + ] + in + let () = Arg.parse arg_spec add_positional_arg usage_msg in + let args = List.rev !args in + ({!opts with vyos_command=(String.concat " " args)}, args) + +let () = + let debug = + (* For simplicity, we check for the existence + of the VYOS_DEBUG environment variable, + rather than for specific values. + *) + match Unix.getenv "VYOS_DEBUG" with + | _ -> true + | exception Not_found -> false + in + let options, args = get_args () in + (* If debug is not enabled by the environment variable, + take it from command line options -- + it may be enabled there. + *) + let () = if debug then print_endline "Debug is enabled by the env var" in + let debug = if debug then true else options.debug in + let () = setup_logging debug in + let op_defs = read_command_definitions () in + let () = Unix.setuid 0 in + try + find_command options ~env:[] ~parent:"" op_defs args + with + | Invalid_command msg -> + Printf.fprintf stderr "Invalid command [%s]: %s" options.vyos_command msg; + exit 1 + | Command_error msg -> + Printf.fprintf stderr "%s" msg; + | Incomplete_command -> + Printf.fprintf stderr "Incomplete command: %s" options.vyos_command; + exit 2 + | Internal_error msg -> + Printf.fprintf stderr "Internal error: %s" msg; + exit 255 + |
