Forum Discussion

AlexS_yb's avatar
AlexS_yb
Icon for Cirrocumulus rankCirrocumulus
Dec 05, 2022

Scope of variables when using call

Hi

I have some irule code that looks like in one file

 

MyLib_v1.0

proc process_URL {} {

log local0. "process_URL: Enter"

if { [ACCESS::session exists] && [ACCESS::session exists -state_allow -sid [ACCESS::session sid]] } {
log local0. "process_URL: Has APM Session"
set durl_ck -1
} else {

log local0. "process_URL: No APM Session"

if { [ HTTP::cookie exists $durl_ck_nm ] } {
log local0. "process_URL: Has Cookie: $durl_ck_nm [HTTP::cookie $durl_ck_nm]"

set durl_flag 1
set durl_url "https://[HTTP::host][HTTP::uri]"
#set durl_url $str
set durl_ck 1
} else {

log local0. "process_URL: No Cookie"

# I can't set the cookie normally have to do a redirect back to myself and set cookie at the same time
ACCESS::disable
set cookie [format "%s=%s; path=/; domain=%s; secure; HttpOnly; Max-Age=1200" $durl_ck_nm $durl_url ".test.com"]
HTTP::respond 302 -version 1.1 Localtion "https://[HTTP::host][HTTP::uri]" "Set-Cookie" $cookie
}

}

}


when RULE_INIT {
# The destination url
set durl_url ""
# Flag 0 do nothing
# Flag 1 Want to dest
set durl_flag 0
# What to do with cookie
# -1 clear cookie in response
# 0 do nothing
# 1 set cookie in response
set durl_ck 0
set durl_ck_nm "SomeCookie.durl"


}

#
# Mark entry points here
when HTTP_REQUEST priority 80 {

# Some reason not calling init ?
set durl_flag 0
set durl_ck 0
set durl_ck_nm "SomeCookie.durl"
}

 

# Do stuff with returning cookie
when HTTP_RESPONSE priority 80 {
if { $durl_ck == -1} {
HTTP::cookie remove $durl_ck_nm
set durl_ck 0
} elseif { ( $durl_flag == 1 ) && ( $durl_ck == 1) } {
HTTP::cookie insert $durl_ck_nm value $durl_url domain ".test.com" path "/"
HTTP::cookie domain $durl_ck_nm ".yieldbroker.com"
HTTP::cookie maxage $durl_ck_nm 1200
HTTP::cookie attribute $durl_ck_nm insert "SameSite" "Strict"
HTTP::cookie secure $durl_ck_nm enable
HTTP::cookie httponly $durl_ck_nm enable
}

}

 

F5 complains when it gets to the code in proc

$durl_ck_nm not set ????

 

plus I had to add to each request

when HTTP_REQUEST priority 80 {

# Some reason not calling init ?
set durl_flag 0
set durl_ck 0
set durl_ck_nm "yb.sso.durl"
}

 

because it loks like RULE_INIT is not running

 

What am I missing ?

 

Oh this is added as a resource to the VS

and there is another irule file that

call the above proc

 

 

 

  • The proc does not inherit the variables from the caller. It will inherit all static variables and functions eg HTTP::path, but there are two ways to allow these variables in the procedure - either send them as calling variables, or use upvar.

    Sending them as variables is more dependable - you don't need to worry about naming or suchlike but you can end up sending a lot of information. Add this to the procedure eg

    proc process_URL {var1 var2 var3 {optionalvar "default value"} } {
    ...
    }

    You can also only have a single variable but make it a list.

    Upvar depends on naming of variables to import a specific variable into the proc scope. Very useful if you want to modify the original variable, but open to errors if the variable doesn't exist or suchlike

    https://www.tcl.tk/man/tcl/TclCmd/upvar.html 

  • Hi AlexS,

    let me give a brief demo what happens behind the scene and how to deal with variable:

    Lets assume this...

     

    when RULE_INIT {
    	log local0.debug "-- RULE_INIT --"
    	set variable_test_1 abc
    	log local0.debug "Exec Level: [info level]"
    	log local0.debug "Visible VAR: [info vars variable_test_*]"
    }
    when HTTP_REQUEST {
    	log local0.debug "-- HTTP_REQUEST (before PROC) --"
    	set variable_test_2 abc
    	log local0.debug "Exec Level: [info level]"
    	log local0.debug "Visible VAR: [info vars variable_test_*]"
    	call TESTPROC
    	log local0.debug "-- HTTP_REQUEST (after PROC) --"
    	log local0.debug "Exec Level: [info level]"
    	log local0.debug "Visible VAR: [info vars variable_test_*]"
    }
    proc TESTPROC { } {
    	log local0.debug "-- TEST PROC --"
    	set variable_test_3 abc
    	log local0.debug "Exec Level: [info level]"
    	log local0.debug "Visible VAR: [info vars variable_test_*]"
    }

     

    Directly after saving the iRule the RULE_INIT event gets triggered and creates you these log lines...

     

    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm1[11166]: Rule /Common/TestRule <RULE_INIT>: -- RULE_INIT --
    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm1[11166]: Rule /Common/TestRule <RULE_INIT>: Exec Level: 0
    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm1[11166]: Rule /Common/TestRule <RULE_INIT>: Visible VAR: variable_test_1
    
    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <RULE_INIT>: -- RULE_INIT --
    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <RULE_INIT>: Exec Level: 0
    Dec  5 11:34:10 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <RULE_INIT>: Visible VAR: variable_test_1
    

     

    Note: I'm using 2 CPU Cores in my Dev Unit, so each of my CPUs running their own TMM process (tmm and tmm1) which will execute the TCL script. Thats why we see 2 times the log...

    When I HTTP request to the virtual server the HTTP_REQUEST will become triggered, which calls the PROC. The HTTP_REQUEST and PROC will both write additional log lines...

     

    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: -- HTTP_REQUEST (before PROC) --
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Exec Level: 2
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Visible VAR: variable_test_2
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: -- TEST PROC --
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Exec Level: 3
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Visible VAR: variable_test_3
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: -- HTTP_REQUEST (after PROC) --
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Exec Level: 2
    Dec  5 11:43:55 kw-f5-dev.itacs.de debug tmm[11166]: Rule /Common/TestRule <HTTP_REQUEST>: Visible VAR: variable_test_2
    

     

    Whats happening in the example above?

    First of all try to think multidimensional!

    We may have lots of CMP-enabled CPUs and each is executing an independent TCL environment. We have global stuff in each independent TCL environment. We have lots of per TCP/UDP connection related stuff going on. And then we have finally individual stuff happening within a PROC executions.

    RULE_INIT scripts are getting executed in the highest TCL execution level (ID=0) and are used to define global stuff which affects all iRules and all Connections processed by your individual Virtual Servers. The purpose of this event is to define static stuff which may become readable by any child execution levels.

    HTTP_REQUEST scripts are getting executed in the TCL execution level (ID=2) of the underlying TCP connection over which the request was send. Whatever you define here, is only affecting the individual TCP connection.

    PROC script are getting executed in a child TCL execution level (ID=MotherLVL+1) of the calling TCL execution level. Whatever you define here, is only affecting the individual PROC itself.

    How can I cross communicate between the TCL execution levels and TCL environments?

    You can define some global stuff in a static::* variable during RULE_INIT. Remember that this code is getting executed once everytime you "Save" your iRule on the System.

     

    when RULE_INIT {
    	set static::optionA "abc"
    }

     

    A HTTP_REQUEST event may read the static::* variable during its event execution.

     

    when HTTP_REQUEST {
    	if { $static::optionA equals "abc" } then {
    		# Do some fancing things...
    	}
    }

     

    Note: Dont change those static::* varibales from events other than RULE_INIT, use them as Read-Only variables. Please simply dont do it... unless you know the impact of doing so... 😉

    A HTTP_REQUEST event may call a PROC and pass arguments to it and the PROC may output a return value which can be stored in a variable.

     

    when HTTP_REQUEST {
    	set my_variable "123"
    	set proc_return_value [call my_proc $my_variable]
    	if { $proc_return_value == "12345" } then {
    		# Do some fancing things...
    	}
    }
    proc my_proc { proc_input_variable } {
    	return "${proc_input_variable}45"
    }
    

     

    A slightly more advanced method to pass varibales between a PROC and its calling execution level would be to use [upvar] to map a varibale within a PROC to a varibale in the calling execution level.

     

    when HTTP_REQUEST {
    	set x "Hello"
    	set y "World"
    	call build_string
    	log local0.debug $z
    }
    proc build_string { } {
        upvar x local_x
        upvar y local_y
        upvar z local_z
        set local_z "$local_x $local_y"
    }

     

    Note: Mapping many varibales between destinct TCL execution levels is annoying and also adds lots of CPU overhead. Use PROCs only if you get advantages by doing so and not because you are getting used to it from other coding languages. I for an example never use procs in TCL because they are almost always better (more flexible, faster, simpler, etc.) alternatives than using PROCs...

    The last method I'd like to show you is how you could cross communicate between independent Connection and even TCL environments running on different CPUs by using the [table] command.

     

    when HTTP_REQUEST {
    	
    	set counter_value [table incr -mustexist "Count_[IP::client_addr]"]
    	if { $counter_value eq "" } then {
    		# Counter not available. Initialize a new table entry with timeout of 10 seconds.
    		set counter_value 1
    		table set "Count_[IP::client_addr]" $counter_value 10 indef	
    	}
    	log local0.debug "Client [IP::client_addr] has requested $counter_value time in the last 10 seconds."
    }
    

     

    Note: The above example will work, even if the consecutive HTTP request where send acrross different TCP connection and processed by different CPUs.

    Good source for the command in this POST:

    Native TCL Command

    F5 specific iRule Commands 

    HTH and Cheers, Kai

    • AlexS_yb's avatar
      AlexS_yb
      Icon for Cirrocumulus rankCirrocumulus

      Thank you a very details and very usefil example

       

      You talk about not using proc's

       

      My need is to do the same thing (steps say 5 which includes a test ) in multiple places on a big switch statement based upon uri.

       

      I can't group the URI together to write this code once and i don't want to duplicate it many times so i thought a call would be the way to do it.

       

      you allude that there are better ways to do it. how would you do that ?

       

      • Kai_Wilke's avatar
        Kai_Wilke
        Icon for MVP rankMVP

        Hi Alex,

        Could you please write some pseudo code how the switch statement looks like and where the proc should be executed? 

        In most cases, you could probably devide your code structure in two destinct parts, the first part parameterizes settings based on conditions (aka. Switch statement assigns variables) and a second  part reads parameters and then executes code conditional. This is probably the most used technique...

        Instead of using [call] procedures you could use [eval] based macros. The difference between those two is that the [eval] macros are executed in the same execution level and can natively access the varibale from the caller. This is actually a slightly rare used technique...


        There are many more options available to avoid the use of TCL procedures. It all depends what you are trying to achive...

        Cheers, Kai

  • The proc does not inherit the variables from the caller. It will inherit all static variables and functions eg HTTP::path, but there are two ways to allow these variables in the procedure - either send them as calling variables, or use upvar.

    Sending them as variables is more dependable - you don't need to worry about naming or suchlike but you can end up sending a lot of information. Add this to the procedure eg

    proc process_URL {var1 var2 var3 {optionalvar "default value"} } {
    ...
    }

    You can also only have a single variable but make it a list.

    Upvar depends on naming of variables to import a specific variable into the proc scope. Very useful if you want to modify the original variable, but open to errors if the variable doesn't exist or suchlike

    https://www.tcl.tk/man/tcl/TclCmd/upvar.html 

    • Kai_Wilke's avatar
      Kai_Wilke
      Icon for MVP rankMVP

      Nevermind Pete... just hit the wrong reply button to answer Alex...

      Cheers, Kai