Session Table Control With iRules

In my previous article titled Session Table Exporting With iRules , I posted an example iRule that will allow you to export your session table entries for archival purposes.   If you are reading this and have no clue what the session table is, you’ll want to read the series titled “The Table Command ” where we walk through all the ins and outs of accessing the session data table.

As I mentioned on a recent podcast, I’ve been planning on adding to that tech tip by including the ability to “import” data back into the session table.   Well, this article includes that, and much more…

I started writing the code and found my self asking a bunch of “what-ifs”. 

What If…

  • I could look at the session table data…
  • I could delete session table entries…
  • I could add session table entries…
  • I could delete session tables…

Instead of drawing this out into multiple tech tips, I decided to go ahead and push forward and include it all in this one.  Previously Jason wrote the “Restful Access to BIG-IP subtables ” article where he build a web service for access into the session tables.  This is great for those programmer geeks, but I figured I’d take a different approach by building a full blown GUI to interact with the session table and this turned out to be pretty easy with iRules!

The iRule

The entire application was written in a single iRule.  By assigning this iRule to a virtual server, you can now make a web request to “http://virtual_ip/subtables” and you will have access to the application.  The virtual server can be a production hosting live applications or a secondary one with no active pools behind it.  For all request not going to “/subtables” it will ignore the request and pass control to additional iRules and then on the assigned pool of servers.

The Application

The logic in this iRule is wrapped around an application name defined in the “APPNAME” variable.  You can change this to whatever you want, but in my example I’ve used the name of “subtables”.  If the URI starts with “/subtables” then the below application logic is processed.  Otherwise, the request is allows to continue on to the backend pool.

1: when HTTP_REQUEST { 
2:   set APPNAME "subtables"; 
3: 
4:   set luri [string tolower [HTTP::uri]] 
5:   set app [getfield $luri "/" 2]; 
6:   set cmd [getfield $luri "/" 3]; 
7:   set tname [URI::decode [getfield [HTTP::uri] "/" 4]]; 
8:   set arg1 [URI::decode [getfield [HTTP::uri] "/" 5]]; 
9:   set arg2 [URI::decode [getfield [HTTP::uri] "/" 6]]; 
10:   set resp ""; 
11: 
12:   set send_response 1; 
13: 
14:   if { $app equals $APPNAME } { 
15:   # Application Logic 
16:   } 
17: }

Command Processing

A little way down in the application logic is the command processor.  There are 4 public commands: edit, export, import, and delete.  These will be described below.  There are also a couple of hidden commands that you will have to dig through the source to look at.

1: #------------------------------------------------------------------------ 
2: # Process commands 
3: #------------------------------------------------------------------------ 
4: switch $cmd { 
5: 
6:   "edit" { 
7:   # Process edit command 
8:   } 
9:   "export" { 
10:   # Process export command 
11:   } 
12:   "import" { 
13:   # Process import command 
14:   } 
15:   "delete" { 
16:   # Process delete command 
17:   } 
18: }

Command: edit

The “edit” command will allow you to view the contents of a subtable.  If no table name is specified, a form is generated prompting the user to enter a valid table name.  If a table name is supplied that currently doesn’t exist on the system, it will act like a create method allowing you to create a new subtable.  After that request is made, the else logic is processed and the edit table is presented along with “X” links after each record to allow you to delete that record, and a final row of edit boxes allowing you to insert a new record into the table.

  1: log local0. "SUBCOMMAND: edit";
  2: if { $tname eq "" } {
  3:   append resp $TABLENAME_FORM
  4: } else {
  5:     append resp "";
  35:    append resp "\n";
  36:  
  37:    append resp "\n";
  38:    append resp "'$tname' Table\n";
  39:    append resp "Key	Value\n";
  40:    foreach key [table keys -subtable $tname] { 
  41:      append resp "$key";
  42:      append resp "[table lookup -subtable $tname $key]";
  43:      append resp "\[X\]"; 
  44:      append resp "\n"; 
  45:  } 
  46:    # Add insertion fields 
  47:    append resp ""; 
  48:    append resp ""; 
  49:    append resp "\[+\]"; 
  50:    append resp "\n"; 
  51:    append resp ""; 
  55: }

Command: export

The export command was taken from my previous article on exporting the session table.  It essentially iterates through all the keys in the subtable and queries their values inserting them into a comma separated list.  The file is then returned via the HTTP::respond command by changing the Content-Type to “text/csv” and specifying a unique file name with the Content-Disposition header.

  1: log local0. "SUBCOMMAND: export";
  2: if { $tname eq "" } {
  3:   append resp $TABLENAME_FORM
  4: } else {
  5:   set csv "Table,Key,Value\n";
  6:   foreach key [table keys -subtable $tname] {
  7:     append csv "${tname},${key},[table lookup -subtable $tname $key]\n";
  8:   }
  9:   set filename [clock format [clock seconds] -format "%Y%m%d_%H%M%S_${tname}.csv"]
  10:   log local0. "Responding with filename $filename...";
  11:   
  12:   set disp "attachment; filename=${filename}";
  13:   HTTP::respond 200 Content $csv "Content-Type" "text/csv" "Content-Disposition" $disp;
  14:   return;
  15: }

Command: import

This logic is the opposite action from the export command.  For the first request to the app, the form is returned containing the fileinput HTML form input item.  The browser will then submit a POST request that has to be parsed out.  The HTTP::collect call is made on the entire POST body and processing continues in the HTTP_REQUEST_DATA event.

  1: if { $tname eq "" } {
  2:   append resp $FILEINPUT_FORM;
  3: } else {
  4:   append resp "SUBMITTED FILE...";
  5:   if { [HTTP::header exists "Content-Length"] } {
  6:     HTTP::collect [HTTP::header "Content-Length"];
  7:     set send_response 0;
  8:   }
  9: }

Processing the File Upload POST Data

In the previous code block, the HTTP::collect call is made.  When the data is buffered up, the HTTP_REQUEST_DATA event is triggered and we can perform the input.  I included some validation in here to make sure this is for the current application request and the import command was requested.    The Payload is then parsed into lines and the parsing fun begins. 

File Upload POST Request

   1: POST /subtables/import/file HTTP/1.1
   2: Host: bigip
   3: User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12
   4: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
   5: Accept-Language: en-us,en;q=0.5
   6: Accept-Encoding: gzip,deflate
   7: Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
   8: Keep-Alive: 115
   9: Connection: keep-alive
  10: Referer: http://bigip/subtables/import/
  11: Cookie: ...
  12: Content-Type: multipart/form-data; boundary=---------------------------190192876111056
  13: Content-Length: 260
  14:  
  15: -----------------------------190192876111056
  16: Content-Disposition: form-data; name="filedata"; filename="20101103_070007_Foo.csv"
  17: Content-Type: application/vnd.ms-excel
  18:  
  19: Table,Key,Value
  20: Foo,1,Hi
  21: Foo,2,There
  22:  
  23: -----------------------------190192876111056--

First, let’s look at the post request and then hopefully the iRule logic will make more sense.  First we have the “Content-Type” header that contains the boundary for the various form elements.  Once we’ve got past all the headers (indicated by the first empty line), I’ll build a little state engine where I set whether I’m “in” or “out” of a boundary.  From within the boundary, I check the Content-Disposition for the “filedata” parameter.  This logic is then looped through until we get to the contents (again delimited by an empty line after the part headers.  Each of those lines are then parsed and the “Table”, “Key”, and “Value” is extracted and the associated “table set” command is called to insert the value into the specified table.

When the processing is complete, a status message is sent to the client indicating the number of records added.

  1: when HTTP_REQUEST_DATA {
  2:   
  3:   log local0. "HTTP_REQUEST_DATA -> app $app";
  4:   if { $app eq $APPNAME } {
  5:     switch $cmd {
  6:       "import" {
  7:         set payload [HTTP::payload]
  8:  
  9:         #------------------------------------------------------------------------
  10:         # Extract Boundary from "Content-Type" header
  11:         #------------------------------------------------------------------------
  12:         set ctype [HTTP::header "Content-Type"];
  13:         set tokens [split $ctype ";"];
  14:         set boundary "";
  15:         foreach {token} $tokens {
  16:           set t2 [split [string trim $token] "="];
  17:           set name [lindex $t2 0];
  18:           set val [lindex $t2 1];
  19:           if { $name eq "boundary" } {
  20:             set boundary $val;
  21:           }
  22:         }
  23:         
  24:         #------------------------------------------------------------------------
  25:         # Process POST data
  26:         #------------------------------------------------------------------------
  27:         set in_boundary 0;
  28:         set in_filedata 0;
  29:         set past_headers 0;
  30:         set process_data 0;
  31:         set num_lines 0;
  32:         if { "" ne $boundary } {
  33:           
  34:           log local0. "Boundary '$boundary'";
  35:           set lines [split [HTTP::payload] "\n"]
  36:           foreach {line} $lines {
  37:             
  38:             set line [string trim $line];
  39:             log local0. "LINE: '$line'";
  40:             
  41:             if { $line contains $boundary } {
  42:  
  43:               if { $in_boundary == 0 } {
  44:                 #----------------------------------------------------------------
  45:                 # entering boundary
  46:                 #----------------------------------------------------------------
  47:                 log local0. "Entering boundary";
  48:                 set in_boundary 1;
  49:                 set in_filedata 0;
  50:                 set past_headers 0;
  51:                 set process_data 0;
  52:               } else {
  53:                 #----------------------------------------------------------------
  54:                 # exiting boundary
  55:                 #----------------------------------------------------------------
  56:                 log local0. "Exiting boundary";
  57:                 set in_boundary 0;
  58:                 set in_filedata 0;
  59:                 set past_headers 0;
  60:                 set process_data 0;
  61:               }
  62:             } else {
  63:               
  64:               #------------------------------------------------------------------
  65:               # in boundary so check for file content
  66:               #------------------------------------------------------------------
  67:               if { ($line starts_with "Content-Disposition: ") &&
  68:                    ($line contains "filedata") } {
  69:                 log local0. "In Filedata";
  70:                 set in_filedata 1;
  71:                 continue;
  72:               } elseif { $line eq "" } {
  73:                 log local0. "Exiting headers";
  74:                 set past_headers 1;
  75:               }
  76:             }
  77:             
  78:             if { $in_filedata && $process_data } {
  79:               log local0. "Appending line";
  80:               
  81:               if { ($num_lines > 0) && ($line ne "") } {
  82:                 #----------------------------------------------------------------
  83:                 # Need to parse line and insert into table
  84:                 # line is format : Name,Key,Value
  85:                 #----------------------------------------------------------------
  86:                 set t [getfield $line "," 1];
  87:                 set k [getfield $line "," 2];
  88:                 set v [getfield $line "," 3] 
  89:                 
  90:                 if { ($t ne "") && ($k ne "") && ($v ne "") } {
  91:                   log local0. "Adding table '$t' entry '$k' => '$v'";
  92:                   table set -subtable $t $k $v indefinite indefinite
  93:                 }
  94:               }
  95:               incr num_lines;
  96:             }
  97:             
  98:             if { $past_headers } {
  99:               log local0. "Begin processing data";
  100:               set process_data 1;
  101:             }
  102:           }
  103:         }
  104:         incr num_lines -2;
  105:         append resp "Successfully imported $num_lines table records"; 
  106: append resp ""; 
  107: HTTP::respond 200 Content $resp; 
  108: } 
  109: } 
  110: } 
  111: }

Command: delete

And, as a final step, I included a “delete” command to allow you to delete all the records from the specified table.  This is done by using the “table delete” command.

  1: log local0. "SUBCOMMAND: delete";
  2: if { $tname eq "" } {
  3:   append resp $TABLENAME_FORM
  4: } else {
  5:   table delete -subtable $tname -all;
  6:   append resp "Subtable $tname successfully deleted"; 
  7: }

The Demo

Below is a little walk through of the application to give you some context of what it looks like to the end user.

The Code

The full code for this article can be found in the iRules CodeShare under SessionTableControl.

Published Nov 03, 2010
Version 1.0

Was this article helpful?

1 Comment

  • This is very helpful!

     

     

    It would be even better if I could see how much time is left for each key-value pair in the table until it will be removed due to aging timeout.

     

    could you add such functionality?