My first go with ocaml-expect has been to write a small utility for something that’s been annoying me for a while at work, which is that there is no straightforward way to associate TNS with DNS (I don’t much fancy using LDAP for everything, DNS is too important). But since Oracle provides the TNSPING
utility, it should be straightforward enough to just piggyback on top of that.
(* SSH from a TNS connect string *) open Unix open Printf open Expect open Str let hostname = ref "" (*{{{ regexp match on tnsping output *) let match_tns tnsentry = let tns_re = regexp_case_fold "HOST[ ]*=[ ]*\(.+\))[ ]*(PORT.*" in try let _ = search_forward tns_re tnsentry 0 in hostname := matched_group 1 tnsentry; true with Not_found -> false (*}}}*)
This is a regular expression for matching the output of TNSPING
, which mirrors that of the corresponding entry in TNSNAMES.ORA
. If it matches it returns true and stores the match.
let main tnsname argv = let ora_home = getenv "ORACLE_HOME" in let tns_cmd = spawn (sprintf "ORACLE_HOME=%s %s/bin/tnsping" ora_home ora_home) [|tnsname|] in let get_hostname = expect tns_cmd [ ExpectFun match_tns, true ] false in match get_hostname with | false -> prerr_endline "TNS entry not matched" | true -> execvp "ssh" (Array.append [| "ssh"; !hostname |] argv)
The main function spawn
s the subprocess, the environment is not inherited from the parent so the necessary variables must be explicitly set. If the match is successful then ssh
in the current $PATH
is executed, with the remaining command line arguments, so it is possible to say tnssh orcl ls
where orcl
is the TNS entry and ls
is the command to run on the remote host.
(*{{{ drop the first n elements of an array *) let drop n arr = try Array.sub arr n ((Array.length arr) - n) with Invalid_argument e -> [||] (*}}}*)
A helper function to extract the remaining command line arguments.
Finally the handling of the command line arguments – if there are none, report an error. If there are 1 or more (in addition to the process name!) then pass the first to TNSPING
and the remaining ones to ssh
.
(*{{{ Handle the command line arguments *) let () = match (Array.length Sys.argv) with |1 -> prerr_endline (sprintf "Usage: %s <TNS name>" Sys.argv.(0)) |_ -> main Sys.argv.(1) (drop 2 Sys.argv) (*}}}*) (* End of file *)
Thoughts: This was pretty easy to write, there is an example in the OCamldoc, the longest task was writing the regular expression! I started out using Str
expecting to be able to get the string match out of the regexp (as in Pexpect) but I couldn’t see an obvious way to do it, hence using ExpectFun (string → bool)
and “hiding” the result I wanted in a mutable string. Next time I’ll just use Pcre with which I am much more familiar (tho’ this creates a dependency on Pcre’s C library being installed wherever this tool is run from).
gaius@debian:~/Projects/Tnssh$ tnsping orcl TNS Ping Utility for Linux: Version 11.2.0.1.0 - Production on 22-SEP-2010 16:00:07 Copyright (c) 1997, 2009, Oracle. All rights reserved. Used parameter files: Used TNSNAMES adapter to resolve the alias Attempting to contact (description = (address=(protocol=tcp)(host=127.0.0.1)(port=1521)) (connect_data=(service_name=orcl))) OK (10 msec) gaius@debian:~/Projects/Tnssh$ ssh orcl ssh: Could not resolve hostname orcl: Name or service not known gaius@debian:~/Projects/Tnssh$ ./tnssh orcl gaius@debian:~$
Not the most amazing example, but I have managed to ssh back to my own machine!
All in all tho’, a useful addition to the toolbox, for exactly the sort of work I want to use OCaml for. Merci, m’sieur Le Gall!
Pcre version:
(* SSH from a TNS connect string *) open Unix open Printf open Expect open Pcre let hostname = ref "" (*{{{ regexp match on tnsping output *) let match_tns tnsentry = let tns_re = regexp "HOST\s+=\s+([\w\d\.-]+)\)" in try let m = get_substrings (exec ~rex:tns_re tnsentry) in hostname := m.(1); true with Not_found -> false (*}}}*) let fail msg = prerr_endline (sprintf "%s: %s" Sys.argv.(0) msg); exit 1 let main tnsname argv = try let ora_home = getenv "ORACLE_HOME" in let tns_cmd = spawn (sprintf "ORACLE_HOME=%s %s/bin/tnsping" ora_home ora_home) [|tnsname|] in let get_hostname = expect tns_cmd [ ExpectFun match_tns, true ] false in match get_hostname with | false -> fail "TNS entry not matched" | true -> execvp "ssh" (Array.append [| "ssh"; "-o"; "StrictHostKeyChecking=no"; !hostname |] argv) with |Not_found -> fail "ORACLE_HOME not set?" (*{{{ drop the first n elements of an array *) let drop n arr = try Array.sub arr n ((Array.length arr) - n) with Invalid_argument e -> [||] (*}}}*) (*{{{ Handle the command line arguments *) let () = match (Array.length Sys.argv) with |1 -> prerr_endline (sprintf "Usage: %s <TNS name> [cmds]" Sys.argv.(0)) |_ -> main Sys.argv.(1) (drop 2 Sys.argv) (*}}}*) (* End of file *)
“De rien Mr Hammond”
I think the next version to come will probably follow the path of ocaml-fileutils:
ExpectRegexp Str.regexp will become ExpectRegexp ‘a and I will specialize the module for Str or Pcre (i.e. you will be able to choose whatever implementation).
I will also allow to pass environment.
If you have other ideas don’t hesitate to contact me.
Two points of style: match_tns shouldn’t use a global ref to store the match, a string ootion would be the appropriate choice. Then, instead of matching on a boolean in main (which is weird) you’d have a bona-fide match on the optional match.
Of course, I assume that expect can use functions that return something else besides true ans false!
Indeed, I am just not sure how to express that in this library. To call
matched_group
requires that you have the string, and the only place you get that string is insidematch_tns
, which must be of typeString → bool
. I can get a string literal out ofexpect
on line 21, but not a string from the regexp.Unless I am missng something very obvious…
Pingback: LDAP (2) | So I decided to take my work back underground