Expect (2)

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 spawns 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 *)

About Gaius

Jus' a good ol' boy, never meanin' no harm
This entry was posted in Ocaml, Oracle. Bookmark the permalink.

4 Responses to Expect (2)

  1. “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.

  2. 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!

    • Gaius says:

      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 inside match_tns, which must be of type String → bool. I can get a string literal out of expect on line 21, but not a string from the regexp.

      Unless I am missng something very obvious…

  3. Pingback: LDAP (2) | So I decided to take my work back underground

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s