I’ve been toying with the idea of building an iControl shell for a long time.  For those who don’t know what I’m talking about, a shell is essentially a fully enclosed application that runs within a console on your computer.  Windows has the Command Prompt and Unix has it’s various shells (sh, bash, ksh, etc) and since most of my working day involves interacting with a shell of some sort, I figured it might be useful to create an interactive BIG-IP shell.

For this example, I picked PowerShell as my language of choice for a couple of reasons: it already is a shell, I have tons of reference samples already, and it’s very extensible.

The Methodology

I wanted to design this application in such a way that it could be actively developed on without having to mess with the core engine.  That way I could add more supported components and methods down the road in a very easy manner.

PowerShell has the concept of user defined “functions” and that ended up being the basis of my modular design.  At the core of the code is a input processor that receives commands from the user and distributes them to the various registered components.  It looks something like this:

   1: while(1)
   2: {
   3:   $prompt = "$(Get-CurrentPath)> ";
   4:   $i = (Read-Host $prompt).Trim();
   5:   
   6:   Debug-Message "< $($MyInvocation.MyCommand.Name) '$i' >";
   7:   if ( $i.Length -gt 0 )
   8:   {
   9:     Debug-Message "CommandLine: '$i'...";
  10:     switch -Wildcard ($i.ToLower())
  11:     {
  12:       ""   { break; }
  13:       "c*" { Initialize-Connection $i; }
  14:       "h*" { Show-Help -Full -Topic (Get-Arguments $i); }
  15:       "i"  { Show-Information; }
  16:       "q"  { Exit-Console; }
  17:       "r"  { Invoke-Expression $LoadModStr; }
  18:       "d"  { $script:DEBUG = -not $script:DEBUG; }
  19:       "!*" {
  20:         $cmd = $i.Replace("!", "").Trim();
  21:         Debug-Message "Executing command $cmd";
  22:         Invoke-Expression $cmd;
  23:       }
  24:       ".." { Move-Up; }
  25:       default {
  26:         if ( $script:Connected )
  27:         {
  28:           if ( Is-SubDirectory $i )     { Move-Down $i; }
  29:           elseif ( Is-ValidCommand $i ) { Process-Command $i; }
  30:           else                          { Show-Help; }
  31:         }
  32:         else
  33:         {
  34:           Show-Help;
  35:         }
  36:       }
  37:     }
  38:   }
  39: }

You’ll see that there are some other functions that I’ve written in there that do various things.

Get-CurrentPath

I wanted to include some sort of navigation into this shell to give the user some context as to where they were in the current command structure.  This is very similar to how the various systems prompts will work with your current directory you are in.  I opted for the format of icsh:/module/interface for my navigation format. 

   1: theboss.dev.net> : ?
   2:             Child Objects
   3:             ----------------
   4:             gtm
   5:             ltm
   6:             networking
   7:             system
   8:  
   9:             Commands In this object
  10:             -----------------------
  11:             gtm/wideip/list
  12:             ltm/pool/list
  13:             ltm/pool/members
  14:             ltm/pool/stats
  15:             ltm/poolmember/disable
  16:             ltm/poolmember/enable
  17:             ltm/poolmember/list
  18:             ltm/virtualserver/list
  19:             networking/vlan/list
  20:             system/systeminfo/list
  21: theboss.dev.net> : gtm
  22: theboss.dev.net/gtm> : wideip
  23: theboss.dev.net/gtm/wideip> : ..
  24: theboss.dev.net/gtm> : ..
  25: theboss.dev.net> : ltm
  26: theboss.dev.net/ltm> : pool
  27: theboss.dev.net/ltm/pool> : ..
  28: theboss.dev.net/ltm> : poolmember
  29: theboss.dev.net/ltm/poolmember> : ..
  30: theboss.dev.net/ltm> : ..
  31: theboss.dev.net> : system
  32: theboss.dev.net/system> : systeminfo

 

Initialize-Connection

This function will ensure that the iControl PowerShell subsystem is configured and will initialize the connection to the BIG-IP.

Show-Help

This function presents the global commands as well as any child objects (ie. “ltm” has “pool”, “poolmember”, “virtualserver”, etc") and the available sub commands.

Show-Information

This is a diagnostic function to print out shell version and various internal variable related to the state of the shell.

Exit-Console

This is a wrapper function to ensure that the users connection information is persisted to the registry so that it can be remembered the next time the shell is started.

Move-Up

Navigate up an object in the hierarchy.

Move-Down

Navigate down to a child of the current object.

Process-Command

 

This is where the fun happens.  But, before I do, it will be a good idea to talk about how objects and methods are injected into the shell.  There is a child directory where the main script resides called Modules.  In there I’ve created a folder hierarchy to match the object hierarchy in the iControl API.  The file system looks like this

   1: c:\icontrolsh\iControlsh.ps1
   2: c:\icontrolsh\modules\gtm
   3: c:\icontrolsh\modules\gtm\wideip.ps1
   4: c:\icontrolsh\modules\ltm
   5: c:\icontrolsh\modules\ltm\pool.ps1
   6: c:\icontrolsh\modules\ltm\poolmember.ps1
   7: c:\icontrolsh\modules\ltm\virtualserver.ps1
   8: c:\icontrolsh\modules\networking
   9: c:\icontrolsh\modules\networking\vlan.ps1
  10: c:\icontrolsh\modules\system
  11: c:\icontrolsh\modules\system\systeminfo.ps1

The script does some file system navigation to determine all the unique “modules” in the modules directory and then for each module, it “dot sources” the child .ps1 files.  “dot sourcing” is essentially running the script and it allows for any variable or function declarations to be included in the current runspace.  The content for the ltm\virtualserver.ps1 script is as follows:

 

   1: # Global Script variables
   2:  
   3: if ( -not $script:ICONTROLSH )
   4: {
   5:     Write-Host "This script is not intended to run standalone.  Exiting...";
   6: }
   7:  
   8: #----------------------------------------------------------------------------
   9: function icsh/ltm/virtualserver/list()
  10: #----------------------------------------------------------------------------
  11: {
  12:   <#
  13:     .SYNOPSIS
  14:     This function allows you to query the list of the virtual servers on a BIG-IP LTM.
  15:  
  16:     .DESCRIPTION
  17:     This function allows you to query the list of the virtual servers on a BIG-IP LTM.
  18:  
  19:     .EXAMPLE
  20:     list
  21:  
  22:     .NOTES
  23:     NAME: icsh/ltm/virtualserver/list
  24:     AUTHOR: Joe Pruitt, F5
  25:  
  26:     .LINK
  27:     http://devcentral.f5.com
  28:   #>
  29:   param($Opts);
  30:   
  31:   Debug-Message "< $($MyInvocation.MyCommand.Name) '$Opts' >";
  32:   $list = (Get-F5.iControl).LocalLBVirtualServer.get_list();
  33:   Write-Host "Virtual Server List";
  34:   Write-Host "-------------------";
  35:   foreach($pool in $list)
  36:   {
  37:     Write-Host "$pool";
  38:   }
  39: }

PowerShell has a built-in help system that I’m able to make use of by including metadata within the <# and #> markers.  You’ll notice that I included a check for a script variable that’s set in the iControlsh.ps1 script so that this script isn’t accidentally run on it’s own.

After all the scripts are “dot sourced”, the builtin Get-Command cmdlet can be used to list all of the defined iControl shell functions.

   1: theboss.dev.net> : !! Get-Command icsh*
   2: CommandType     Name
   3: -----------     ----
   4: Function        icsh/gtm/wideip/list
   5: Function        icsh/ltm/pool/list
   6: Function        icsh/ltm/pool/members
   7: Function        icsh/ltm/pool/stats
   8: Function        icsh/ltm/poolmember/disable
   9: Function        icsh/ltm/poolmember/enable
  10: Function        icsh/ltm/poolmember/list
  11: Function        icsh/ltm/virtualserver/list
  12: Function        icsh/networking/vlan/list
  13: Function        icsh/system/systeminfo/list

This get’s us back to the Process-Command function.  With a little string manipulation it’s fairly easy to parse the function names for valid child elements as well as invoking the method calls with the PowerShell Invoke-Expression cmdlet.

   1: #----------------------------------------------------------------------------
   2: function Process-Command()
   3: #----------------------------------------------------------------------------
   4: {
   5:   param($cmd);
   6:   Debug-Message "< $($MyInvocation.MyCommand.Name) >";
   7:   $tokens = $cmd.Split(" ");
   8:   $command = $tokens[0];
   9:   $args = "";
  10:   for($i=1; $i-lt$tokens.Length; $i++)
  11:   {
  12:     if ( $i -gt 1 ) { $args += " "; }
  13:     $args += $tokens[$i];
  14:   }
  15:   $key = $(Get-CurrentPath) + "/" + $command;
  16:   $code = Gen-FunctionName $command;
  17:   
  18:   Debug-Message "Processing command: '$code' '$args'";
  19:   if ( $args.Length -gt 0 ) { $code += " -Opts '$args'"; }
  20:   Debug-Message "Invoking code <$code>";
  21:   Invoke-Expression $code;
  22: }

 

 

 

The Full Source

The source for this application can be found in the iControl CodeShare under “PsBigipInteractiveShell”.

Video Walkthrough

There’s a lot of code to review in an article like this and I’m going to leave it to you all to view the source in the wiki.  In the meantime, you can watch this short video I’ve recorded illustration a sample session within the shell.

 

Related Articles on DevCentral

Technorati Tags: shell, iControl, PowerShell, BIGIP
Comments on this Article
Comment made 03-Nov-2010 by L4L7 69
Very, very nicely done Joe - impressive stuff!
0