|
| 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