The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Expect::Angel - Build up a robust connection class to your DUT (Router/Switch/Host)

SYNOPSIS

  use Expect::Angel;
  @ISA = ("Expect::Angel");
  sub new {
     my $type = shift;
     my $para = {@_};
     my $conn = $type->build(%$para);
     ... ...
     return $conn;
  }

  # move to config mode of the device
  sub configMode {
     my ($conn) = shift;
     $conn->movState("config") or return undef;
  }

  # at your derived module, you can easily do the following jobs.
  # capture output of a command execution
  my @ints = $conn->cmdexe("show int brief");

  # executes a list of commands at "config" mode of Cisco device
  my @cmd = ("interface e0/0",
             "ip address 10.1.1.10 255.2555.255.0",
             "no shut",
             "exit",  
             "interface e0/1",
             "ip address 10.1.2.100 255.2555.255.0",
             "no shut"
            );
  $conn->cmdexe(\@cmd, "config");

  # always stay at config mode for each command execution, although
  # existing some command that may change mode inadvertently.
  $conn->sticky(1);
  my @cmd = ("interface e0/0",
             "ip address 10.1.1.10 255.2555.255.0",
             "end",  # NOTE, this cause the transition to "enable" mode
             "interface e0/1",
             "ip address 10.1.2.100 255.2555.255.0",
             "no shut"
            );
  $conn->cmdexe(\@cmd, "config");
  

DESCRIPTION

If you are looking for a module that can help you to quickly build up a robust connection to your DUT (Device under Test), here is the right place!

Angel is just like a messenger between your testing machine, where everyting is perfect like heaven, and DUT, where exists right and wrong like earth. The messenger must tolerate the errors and disasters occuring on DUT and faithful deliver information to and from the DUT.

Angel is built on Expect.pm module, but hides the complexity of it. With Angel.pm You can easily build an object-oriented module that meets your specific need with the most useful methods inherited from Angel.

Let's take CISCO router as the first example.

 CISCIO Router

 Modes:  Non-privilege  --- (enable) ---> Enable Mode -- (config t) --> Config Mode

                        <-- (exit) ------             <-- (exit) -----

Here the router has 3 modes, Non-privilege, Enable, and Config. When executing enable command at 1st mode, it transits to Enable mode, and so on as shown.

General speaking, DUT is a network appliance that provides CLI (command line interface) for user to configure and manupulate. It's very often that a DUT presents multiple states, each grants a level of privileges.

  DUT:    state0  ---->  state1  ----->  state2 ... ... -----> stateN   
                  <----          <-----                 <-----

Each state is a stable mode of the DUT, at which the DUT may accept an input and execute it as a command. Each state has its own prompt and a set of commands appropriate to this state. The commands under a state can be put into categories.

  1. After execution the state keeps the same. 

  2. After execution the state transits to another one. 

  3. The execution will show more prompt asking for more input, and eventually back to the some state.

The state transition may happen between two adjacent states, or skip some states.

Your task is to tell Angel the following information for a specific DUT.

  1. describes the states and their prompts

  2. describes the state transition

Angel provides you:

 1. Maintain the connection to DUT.

 2. Send command to DUT at each state and retrieve response of the command.

 3. Error handling.

 4. Log the messages exchanged.

constructor - build({ key1 => val1, ... , keyN => valN})

  Function: create the DUT object.

  - build({ timeout => $in_seconds,     # default is 30 seconds
            errMode => 'die|return|$handler ' # default is die, you can provide your own handler
            errTries => $number,        # default is 3
            log => $file,               # default is ./Angelfile.log, you can put filename|filehandle
            debug => 1,                 # default is 0, Angel debug info
            debugExp => 1,              # default is 0, Expect debug info
            liveExp => 1,               # default is 1, Expect's log_user
            expRaw  => 1,               # default is 1, Raw mode of terminal driver.
            user    => user_name        # used to connect to DUT. If none provided,
                                          will be asked in from keyboard 
            passwd   => password        # used to connect to DUT. If none provided, 
                                          will be asked in from keyboard
            aggressive => 0|1           # default is 0, see explanation below.
            others => values            # see below
          })

 Function: create the DUT object 

   Some parameters are self explained.

   timeout - after a command is sent to DUT, the response is expected to return without it.
   Longer than timeout is regarded as a failure and counted in errTries.

   log - indicates the target of log created by this module and Expect. The log may contain 
   all commands and intermediate prompts between Angel and DUT as well as Expect debug 
   information that are controlled by "debug" and "debugExp". The log message will be 
   written to the file specified by file_name, or File_handle if you have your own log 
   framework. If none is seen, Angelfile.log at current directory is used.

   aggressive - controls the state transition behavior. There are two ways in transition.
      - aggressive
       If transition is defined from a state to another, it will try to move directly.
       otherwise, try one hop backward of the target.
           for example, current state = 1, target state = 4
           if defined transition to state 4, then go directly;
           otherwise, check if transition to state 3 is defined; if not, then try state 2, and so on.

      - non_aggressive (default)
       similiar to aggressive, but it tries one hop forward from the source
       i.e. if 1 to 4 fails, it tries to go to 2 first, then 2 to 4.

      This may affect you state transition design. The design should meet that there is a path
      from any source state to any target state.

   liveExp - controls live output from spawned process such as telnet or ssh. it's Expect's log_user().
   With this set to 1, you can see all the information exchanged by Angel.

   debugExp - controls expect debug message. If you turn it on, there will be huge Expect debug 
              messages printed out on screen

   others:
   sticky - controls state transition behavior after a command execution, see cmdexe() for detail.
   you can change it any time.

   ignoreSecWarn - If you run with debug enabled, the password is printed to log target,
   This may be a security exposure. So the default behavior is that Angel will print a 
   warning and wait for your confirmation on keyboard to continue. If you don't want this
   stop, you can set ignoreSecWarn => 1.

   defPattern - This is the default Expect pattern and action pairs.
   You can define the most common patterns here, which is added to the Expect body of
   all state transition and command execution, so that you don't need to include them in
   each state transition description or command. If you do define the patern at each individule
   one, it takes precedence over the default pattern. 
   One example is like this
     defPattern = [ { '[-]+\s*(More|more)\s*[-]+' => ' ',
                       '^.* memory\? *\[confirm\] *$' => '\r'},

                     { '^.*Are you sure\?.*\[confirm\] *$' => '\r',
                       '^.*Are you sure\?.*$'              => 'yes\r',
                       '^.*Are you sure you want to continue connecting\?.*$' => 'yes\r',
                     }
                   ]
   Its data structure is list of hash like this: [ {}, ... {} ]
   The list keeps the sequence in the pattern match, while the order does not matter in a hash.
   In another word, if the pattern match order does matter, you put them in different hash
   in preferred order, otherwise you can put them in the same hash.
   
   noechoback - controls if a command is expected to be echoed back. By default it doesn't exist
   and the command sent to DUT is expected to be back. This is true for Cisco and Juniper devices.
   In case your situation doesn't have this behivior, you can set this attribute to any value. 
   see sendCmd() method for more infomation.

addState(name => state_name, descr => "blar blar", prompt => 'switch \r?$')

 Function: to add a state 
    If a value not defined, it will be stateN, where N is "state seqence number"
    The initial state, which is 0, will use descr => "initial state", prompt => 'MATCHNOTHING" by default.
 return state sequence number of this state

 descr: description of this state, used for human readable purpose only.
 name: name of the state, unique in all states, default states would be 0, 1, 2, etc. 
       State is identified by either the sequence number (0 is the initial state), or the name.
 prompt: prompt of this state, in perl regular expression

 After the call, a state data structure is appended to state list.
 state => [ # no.0 state 
            { name => "state_name",  # must have letter, state0 is the default
                                    # for 'initial state.
              descr => "Printable strings"  # purely for human readable
                                            # 'initial state' for 1st state by default
              prompt => reg(RE); # regular expression of the state
                                 # this is to match prompt of the socket
            },

            # more state
            { },
         ]

transitState($from,$to,$transDefinition)

   Add a transition definition from $from state to $to state.
   $from: source state name or number 
   $to:   destination state name or number
   $transDefinition: ref to hash body defining how to do the transition
   return: previous transition definition from $from to $to if defined, or undef

   $transDefinition is defined like this
     { command => "executable command at the state",
       expect => [ { hint/answer block }, ... {} ]
     }
     or
     { command => [("one command","another command", ... ,"last cmd")],
       expect => [ { hint/answer block }, ... , {} ]
     }

     hint/answer bloc
     could be none, {}, means no interactive action duration transition
     hint => reg(RE);  # regular expression for system response to go to that state
                       # password is one example
     answer => "answer to the hint in order to go that state", "\r" or "\n" is auto added

     or

     { nexthop => state_name or number }

     They are mutual exclusive. If both exists, nexthop will take precedence, i.e, try to
     go to the state first, then make follow whatever defined at that state.

   Note to $transDefinition -- This ref is directly assigned to the internal data structure
   without deep copy. It is caller's responsibility to assign a new memory location for each
   call as shown above.

catState

    Print all the defined states for debug. 
    Note, the credential may be printed out by this

movState($state)

 Move to $state from whatever current state is. It will try its best anyway up to errTries times.
 After the first time fails, it moves state to initial state, then go to $state afterwards. 
 If errTries times has been tried and still failed, it keeps the best achieved state and return undef.
 return: true if successful, new current state = $state
         undef otherwise

sticky()

   set or get sticky attribute of the object.
   sticky controls state transition behavior after a command execution, see cmdexe() for detail.
   input: optional, true|false, it's set if provided, otherwise it returns the current value.

state0

  Close the socket and set the object to initial state. It doesn't try to go over 
  the state transition from current to 0 like movState(0) does, instead, it directly 
  calls soft_close() of Expect to close the connection.
  But it keeps all the object properties and state transition date, so it helps to 
  re-transit to a state from initial in case it has experienced a problem in previous 
  attempt.   If a decent goodbye is required, movState(0) is the best solution.

cmdexe(cmd,expect,state)

 Execute the command(s)
 cmd: scalar or list, means one command or a list of command to be executed
 expect: optional, ref to list of hash that describes the interactive expect body for this/these commands.
         [ { pattern1 => action1 }, ... {} ]
         If cmd is a list, then expect will be used in execution of all the commands in the list.
 state: optional, specifies the state at which the command(s) to be executed.
         Default is current state.
         If specified, cmdexe will go to the state and execute the command(s)
 return: the output of the last command execution. In list context it returns line by line,
         without \n, in scalar context, it returns the string of the output.
 Notes: The starting state is either specified by $state or current state. 
        The state may change during the cmd execution. If object's "sticky" is set, 
        it always tries to go back to starting state after each cmd execution. 
        Otherwise, it leaves whatever state it is cuased by the cmd execution. 
        So this attribute affects the rest of command(s).

 In case error happens when a command is executed, it will try errTries times (defualt is 3),
 each time it moves state back to 0, and goes to starting state, and executes the command again. 
 The state transition failure is counted in errTries. The errTries is reset for each command. 
 If errTries times  has been tried and still failed, it takes action defined by errMode, 
 i.e, die, return, or exectues a code, then return false condition.
       

cmdSendCheck($cmd,$errorPattern,$interactiveBody)

 send $cmd and check if the command is complained by device.

 Some device rejects a command and feeds back some error message. If you want to detect 
 this, you can monitor the error message pattern from the real device. For example, Cisco
 switch prints something like "% error_message". Then, you specifiy $errorPattern, which
 is a regular expression in this method call.

 This method will exectues $cmd at current state and detect if $errorPattern matches its 
 output in each execution. If it is, meaning the command is rejected, it will return 
 immediatedly with undef. Otherwise it continues and return the real output of the execution
 of the last command.

 $cmd: single or ref to a list of commands.
 $errorPattern: RE of error pattern, like '^%' in cisco switch.
 $interactiveBody: optional, same as $expect defined in cmdexe() method, please see cmdexe().
 return: undef when $errorPattern matches, otherwise output of the last command

echo($cmd) echo($cmd,$expect)

 This method is deprecated in this version. Use cmdexe() instead.

 It executes $cmd at current state, and return the response of DUT in either array, or ref of the array. 

 It always expects current state's prompt after executes $cmd. If a command may cause 
 state transition, it's safe to use cmdexe(), which will tolerate any state match.

 some $cmd need to interact with DUT in terms that DUT waits for some input after 
 accepting $cmd. Multiple prompt phrases and answers may be exchanged before the $cmd 
 is done and final prompt shows up.

 One example is like '--More--' is shown up during the output, and this case can be defined 
 in def_pattern, which will be taken care of in the interactive way during this process

 $expect defines these interactive exchanges specifically for this $cmd.
  {"prompt phrase1 => answer1", ... , "prompt phraseN => answerN"}
  where "prompt phraseN (N = 1 ... N)" can be expressed in Regular Expression

 In case error happens when $cmd is executed, it will try errTries times (defualt is 3), 
 each time it moves state back to 0, and goes to current state, and executes the $cmd again. 
 If errTries times  has been tried and still failed, it takes action defined by errMode, 
 i.e, die, return, or exectues a code, then return false condition.

 The response from the execution of the command $cmd is returned, each line is an element. 
 Depending on the context to assign, it either returns reference or the list itself.

 # this will return a list and assigned to @msg;
   @msg = $conn->echo("ls -l");

 # this will return ref to a list and assigned to $msg;
   $msg = $conn->echo("ls -l");
       

robustecho($cmd,$retMsgPt,$rejectPattern)

 This method is deprecated in this version. Use cmdSendCheck() instead.

    Send an command to DUT may encounter 3 situations
    1. the command is slurped by DUT without rejection, the action success.
    2. the command is rejected by DUT with error message, the action is failed
    3. DUT is crashed by the command, the action cause catastrophe

    This method detects the situation and return true on No.1, false on No.2, and undef on No.3
    $retMsgPt is a ref to list, the response of DUT to $cmd is assiged to it.
    $rejectPattern is RE that tells the matched response is regarded as rejection message from DUT.
    If the No.3 happens, $retMsgPt will hold the error message.

sendCmd($cmd, $timeout, $retry)

Send $cmd and check the echo back of $cmd After sending $cmd, it checks the echo-back of $cmd. If it sees echo-back, the operation is regarded as success and returns 1. Otherwise by default it will try another time with slow speed to see if this can be done, unless $retry is provided. If all tries failed, it die out.

This method is necessary for two reasones. - Make sure the connection is good - The command itself is eliminated from the output of the command.

This process can be skipped if your connection doesn't echo back command, in such case, you must set the object attribute "noechoback";

input: $cmd - command to be executed $timeout - optional, default is the class's timeout, see build() timeout to wait for the echo-back happens. $retry - optional, default is 2, the number of repeat to make it success.

See also

  Expect::Angel::Cisco
  Expect::Angel::Juniper
  Expect::Angel::Linux

Changes

 2007 verison 0.02
 2010 verison 1.00

AUTHOR

Ming Zhang <ming2004@gmail.com<gt>

COPYRIGHT AND LICENSE

Copyright (C) 2010 by ming zhang

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.5 or, at your option, any later version of Perl 5 you may have available.