Closed tthomas0702 closed 6 years ago
@tthomas0702 it's complicated.
There is no such API to just "give it an iApp template". The API instead requires that you provide all of the fields of an iApp template. This means you need to essentially split apart a given iApp template into its component parts and then POST a JSON payload whose fields contain those component parts.
JSON is required, but the fields would hold the APL.
If you query this URL,
You can see the following
{
"kind": "tm:sys:application:template:templatecollectionstate",
"selfLink": "https://localhost/mgmt/tm/sys/application/template/example?ver=13.1.0",
"items": [
{
"propertyDescriptions": {
"actions": "Manage the set of actions associated with an application template.",
"description": "User defined description.",
"ignoreVerification": "Set to true to temporarily stop the verification of signature or checksum. Signature or checksum is still retained.",
"requiresBigipVersionMax": "Specifies the maximum version of BIG-IP required by this template.",
"requiresBigipVersionMin": "Specifies the minimum version of BIG-IP required by this template.",
"requiresModules": "Adds, deletes, or replaces the list of modules that are required to be provisioned or enabled for this template to work.",
"signingKey": "The private key to use for signing the template. Only for use with the generate signature command.",
"tmplChecksum": "Computes a checksum for the script.",
"tmplSignature": "Sign the script using the specified private key and corresponding public certificate."
},
"actions": {
"isSubcollection": true,
"propertyDescriptions": {
"htmlHelp": "The help for the application template action formatted as HTML.",
"implementation": "The script that is executed to create the configuration objects associated with the application.",
"macro": "",
"presentation": "Specifies what questions must be answered to create an application from the template.",
"roleAcl": "List of roles that are allowed to execute the action.",
"runAs": "The user account that will be used to execute the implementation script. If no account is specified, the script is executed as the calling user."
}
},
"description": "",
"ignoreVerification": "false",
"requiresBigipVersionMax": "",
"requiresBigipVersionMin": "",
"requiresModules": [],
"signingKey": "",
"tmplChecksum": "",
"tmplSignature": "",
"naturalKeyPropertyNames": [
"name",
"partition",
"subPath"
]
}
]
}
You can see that there are a number of fields there, some of them which relate to the sections of an iApp itself. So, the hard part is left for the user to figure out unfortunately.
As you might imagine, trying to split up an iApp template is nearly impossible due to the bracing and escaping of bracing that may be codified in them. It's not totally impossible, but its not just template.split("{")
either.
For this reason, the Ansible modules use a different, arguably more simple, approach. Shown here
We upload the template, and then use tmsh over REST to install it. This has the added advantage of picking up (automatically) and procs that you may define outside of the "sys application template FOO { ... }" blob (this inclusion is used heavily by iApps that F5 publishes and supports.
So I would recommend this alternate approach for what you want to do.
Lemme know if you have any other questions
@caphrim007 I need to make sure I understand.
My goal is to upload an unaltered template to a BIG-IP. This would just be added to the list of available templates but not deployed at this point as an iApp.
Is there a technical reason we cannot do this or is it just not a feature we have yet?
I can do this using the REST API.
@tthomas0702 can you post the full POST payload that is sent? You left some off
@caphrim007
{
"kind": "tm:sys:application:template:templatestate",
"name": "appsvcs_integration_v2.0.004",
"partition": "Common",
"fullPath": "/Common/appsvcs_integration_v2.0.004",
"generation": 6,
"selfLink": "https://localhost/mgmt/tm/sys/application/template/~Common~appsvcs_integration_v2.0.004?expandSubcollections=true&ver=13.0.0",
"ignoreVerification": "false",
"requiresModules": [
"ltm"
],
"totalSigningStatus": "not-all-signed",
"verificationStatus": "none",
"actionsReference": {
"link": "https://localhost/mgmt/tm/sys/application/template/~Common~appsvcs_integration_v2.0.004/actions?ver=13.0.0",
"isSubcollection": true,
"items": [
{
"kind": "tm:sys:application:template:actions:actionsstate",
"name": "definition",
"fullPath": "definition",
"generation": 6,
"selfLink": "https://localhost/mgmt/tm/sys/application/template/~Common~appsvcs_integration_v2.0.004/actions/definition?ver=13.0.0",
"implementation": "# Copyright (c) 2017 F5 Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\npackage require base64\n\nset startTime [clock seconds]\nset bundler_timestamp [clock format $startTime -format {%Y%m%d%H%M%S}]\n\nset NAME \"F5 Application Services Integration iApp (Community Edition)\"\nset TMPLNAME \"appsvcs_integration_v2.0.\"\nset IMPLMAJORVERSION \"2.0\"\nset IMPLMINORVERSION \"004\"\nset IMPLVERSION [format \"%s.%s\" $IMPLMAJORVERSION $IMPLMINORVERSION]\nset POSTDEPLOY_DELAY 0\n\nif { [tmsh::get_field_value [lindex [tmsh::get_config sys scriptd log-level] 0] log-level] eq \"debug\" } {\n set iapplogLevel 10\n}\n\n# Print a timestamped debug message to /var/tmp/scriptd.out\n# Input: headers = TCL list of headers for the log message\n# msg = The message to log\n# level = Integer indicated the log level for this message\nproc debug { headers msg level } {\n if { $::iapplogLevel >= $level } {\n set systemTime [clock seconds]\n set brackets \"\"\n if { [llength $headers] > 0 } {\n set brackets [format \"\[%s\]\" [join $headers \"\]\[\"]]\n }\n set pre [format \"\[%s %s\]\[%s\]%s\" [clock format $systemTime -format %D] [clock format $systemTime -format %H:%M:%S] $::app $brackets]\n puts [format \"%s %s\" $pre [string map [list \"\n\" \"\n$pre \" ] $msg]]\n }\n}\n\n# Credit for psplit: http://wiki.tcl.tk/1499\n# Perform the equivalent of a split on a string except protect an escaped split character in the input\n# Input: str = the string to split\n# seps = the charater(s) to split by\n# Return: list $strings\nproc psplit { str seps {protector \"\\\"}} {\n set out [list]\n set prev \"\"\n set current \"\"\n foreach c [split $str \"\"] {\n if { [string first $c $seps] >= 0 } {\n if { $prev eq $protector } {\n set current [string range $current 0 end-1]\n append current $c\n } else {\n lappend out $current\n set current \"\"\n }\n set prev \"\"\n } else {\n append current $c\n set prev $c\n }\n }\n\n if { $current ne \"\" } {\n lappend out $current\n }\n\n return $out\n}\n\n# Figure out which type of environment we are executing in.\n# Return: list $mode $folder $partition $routedomainid $newdeploy\n# Modes: 1 = Standalone\n# 2 = iWorkflow UNUSED/LEGACY\n# 3 = Cisco APIC\n# 4 = VMware NSX\nproc get_mode { } {\n set folder [tmsh::pwd]\n set app $tmsh::app_name\n set partition [lindex [split $folder /] 1]\n set newdeploy [catch {tmsh::get_config sys application service /$partition/$app.app/$app}]\n debug [list get_mode] [format \"starting folder=%s partition=%s newdeploy=%s\" $folder $partition $newdeploy] 10\n\n if { ! $newdeploy } {\n set ::asoobj [lindex [lindex [tmsh::get_config sys application service /$partition/$app.app/$app] 0] 4]\n }\n # Set the routedomain to the partition default-route-domain\n if { [string tolower $::iapprouteDomain] eq \"auto\"} {\n set obj [tmsh::get_config auth partition $partition default-route-domain]\n set routedomainid [tmsh::get_field_value [lindex $obj 0] default-route-domain]\n debug [list get_mode set_route_domain] [format \"Using partition default-route-domain; routedomainid=%s\" $routedomainid] 10\n } else {\n set routedomainid $::iapprouteDomain\n debug [list get_mode set_route_domain] [format \"Using route domain override; routedomainid=%s\" $routedomainid] 10\n }\n\n # Check for a mode override in $iappmode variable\n if { [string tolower $::iappmode] ne \"auto\" } {\n if { $::iappmode > 0 && $::iappmode < 4 } {\n debug [list get_mode modeoverride] [format \"Mode override detected. Setting mode to %s\" $::iappmode] 10\n return [list $::iappmode $folder $partition $routedomainid $newdeploy]\n } else {\n error \"The mode override specified is invalid.\"\n }\n }\n\n # Check for a partition that starts with apic and return APIC mode (3) and RD if found\n if { [string match -nocase \"apic_\" $partition] || [string match -nocase \"apic-\" $partition] } {\n debug [list getmode apic] \"partition starts with apic, assuming APIC deployment mode (3)\" 10\n set rdobjs [tmsh::get_config net route-domain \"/$partition/$partition\" id]\n set routedomainid [tmsh::get_field_value [lindex $rdobjs 0] \"id\"]\n debug [list getmode apic] [format \"rdobjs=%s routedomainid=%s\" $rdobjs $routedomainid] 10\n return [list 3 $folder $partition $routedomainid $newdeploy]\n }\n\n # Check for an $app name that is formatted like this:\n # edge-<#><#>virtualserver-<#>-serviceprofile-<#>\n # and return NSX mode (4)\n if { [regexp -nocase {^edge-[0-9]+[0-9]+_virtualserver-[0-9]+-serviceprofile-[0-9]+$} $::app] } {\n debug [list get_mode nsx] \"app name matches NSX regexp, assuming NSX deployment mode (4)\" 10\n return [list 4 $folder $partition $routedomainid $newdeploy]\n }\n\n # Default is Standalone mode\n debug [list get_mode standalone] \"no integration vendor found, assuming Standalone deployment mode (1)\" 10\n return [list 1 $folder $partition $routedomainid $newdeploy]\n}\n\n# Create a specfic option command and return it\n# Input: $debug_id, $input_var, $option_string\n# Return: string $cmd\nproc generic_add_option { debug_id input_var option_string custom_format replace_commas } {\n set cmd \" \"\n if { [string length $input_var] > 0 } {\n if { $replace_commas == 1 } {\n set input_var [string map {\",\" \" \"} $input_var]\n }\n\n if { [string length $custom_format] > 0 } {\n set cmd [format $custom_format $input_var]\n } else {\n set cmd [format \" $option_string \\"%s\\"\" $input_var]\n }\n debug [lappend debug_id generic_add_option] [format \"cmd=%s\" $cmd] 10\n }\n return $cmd\n}\n\n# Check to see if an ip has a routedomain included.\n# Return: 0=false; 1=true\nproc has_routedomain { ip } {\n return [string match % $ip]\n}\n\n# Replace a profile within a virtual server definition while preserving the existing context\n# Input: $obj = tmsh obj representing profiles section of the VS get_config\n# $oldprofile = name of the profile to replace\n# $newprofile = name of the new profile\n# Return: string $newprofiles (string suitable for providing to replace-all-with option)\nproc replace_profile { obj oldprofile newprofile } {\n set profiles [tmsh::get_field_value [lindex $obj 0] \"profiles\"]\n set newprofiles \" { \"\n foreach profile $profiles {\n set junk [lindex $profile 0]\n set name [lindex $profile 1]\n set contextobj [lindex $profile 2]\n set context [lindex $contextobj 1]\n if { $name eq $oldprofile } {\n debug [list replace_profile] [format \"replace profile '%s' with '%s' context=%s\" $name $newprofile $context] 10\n append newprofiles [format \"%s { context %s } \" $newprofile $context]\n } else {\n debug [list replace_profile] [format \"preserve profile '%s' context=%s\" $name $context] 10\n append newprofiles [format \"%s { context %s } \" $name $context]\n }\n }\n append newprofiles \" } \"\n return $newprofiles\n}\n\n# Look at a tmsh profile object and determine if $option is a valid profile option\n# Input: $obj = tmsh obj to check\n# $option = option name to look for\n# Return: 1=Valid option; 0=Invalid option\nproc is_valid_profile_option { obj option } {\n debug [list is_valid_profile_option obj] [format \"%s\" $obj] 11\n debug [list is_valid_profile_option option] [format \"looking for %s\" $option] 11\n set found 0\n set fdx 0\n set fields [tmsh::get_field_names value $obj]\n set fields2 [tmsh::get_field_names nested $obj]\n debug [list is_valid_profile_option fields] [format \"%s\" $fields] 11\n debug [list is_valid_profile_option fields2] [format \"%s\" $fields2] 11\n set field_count [llength $fields]\n while { $fdx < $field_count } {\n set field [lindex $fields $fdx]\n if { $field == $option } {\n return 1\n }\n incr fdx\n }\n set field_count [llength $fields2]\n set fdx 0\n while { $fdx < $field_count } {\n set field [lindex $fields2 $fdx]\n if { $field == $option } {\n return 1\n }\n incr fdx\n }\n return 0\n}\n\n# Process a string in the format key1=val1[;keyX=valX] and return an array\n# Input: $string = string to process\n# Return: array { key1 {val1} ... keyX{valX}}\nproc process_kvp_string { string } {\n debug [list process_kvp_string] \"processing string: $string\" 10\n set pairs [psplit $string \";\"]\n array set ret {}\n foreach pair $pairs {\n set key [lindex [split $pair =] 0]\n set val [lindex [split $pair =] 1]\n set ret($key) $val\n debug [list process_kvp_string] \"pair=$pair key=$key val=$val\" 10\n }\n return [array get ret]\n}\n\n# Create an object name\n# Input: $append = string to append\n# Return: $string\nproc create_objname { append } {\n return [format \"%s/%s%s\" $::app_path $::app $append]\n}\n\n# Safely change a variable to a new value. Updates the var value and modifies the ASO with the new value\n# Input: $name = name of variable\n# $value = new value of the variable\n# Return: none\nproc change_var { name value } {\n debug [list change_var] \"updating variable $name to $value (executes post-deployment)\" 10\n set varcmd [create_escaped_tmsh [format \"tmsh::modify sys application service %s/%s variables modify \{ %s \{ value \\"%s\\" \} \}\" $::app_path $::app $name $value]]\n debug [list change_var tmsh_modify_deferred] $varcmd 1\n lappend ::postfinal_deferred_cmds $varcmd\n set [subst ::$name] $value\n set ::aso_config($name) $value\n return\n}\n\n# Check to see if an incoming variable is different than whats stored in the ASO.\n# Input: $name = name of variable\n# Return: 1=value is different; 0=value not different OR not a redeploy\nproc is_new_value { name } {\n if { $::newdeploy } {\n return 0\n }\n set varvalue [get_var $name]\n debug [list is_new_value] [format \"name=%s asovalue=%s varvalue=%s\" $name $varvalue [set [subst ::$name]]] 10\n if { [set [subst ::$name]] == $varvalue } {\n return 0\n }\n return 1\n}\n\n# Get the variable value in the ASO.\n# Input: $name = name of variable\n# $orig = return the original value, not the runtime updated one\n# Return: $string = value of variable\nproc get_var { name { orig 0 }} {\n if { $::newdeploy == 1} {\n return \"\"\n }\n debug [list get_var] [format \"start name=%s\" $name] 10\n\n if { $orig == 0 && [info exists ::aso_config($name)] } {\n set varvalue $::aso_config($name)\n debug [list get_var] [format \"name=%s value=%s\" $name $varvalue] 10\n return $varvalue\n }\n\n if { $orig == 1 && [info exists ::aso_config_orig($name)] } {\n set varvalue $::aso_config_orig($name)\n debug [list get_var original] [format \"name=%s value=%s\" $name $varvalue] 10\n return $varvalue\n }\n return \"\"\n}\n\n# Safely handle the removal of a virtual server option on redeployment\n# Input: $name = name of variable\n# $checkvalue = the string that disables the option\n# $option = TMSH name of the option\n# $module = the BIG-IP module that enables the option\n# Return: 1=Option removed; 0=no action taken\nproc handle_opt_remove_on_redeploy { name checkvalue option module } {\n if { ! $::redeploy || $::pooladdr eq \"255.255.255.254\" } {\n debug [list handle_opt_remove_on_redeploy $name] \"not a redeployment, skipping\" 10\n return 0\n }\n\n if { ! [is_provisioned $module] } {\n debug [list handle_opt_remove_on_redeploy $name] [format \"%s not provisioned, skipping\" $module] 10\n return 0\n }\n\n set vsname [get_var vsName 1]\n set vsobj [lindex [tmsh::get_config ltm virtual $::app_path/$vsname all-properties] 0]\n if { [is_valid_profile_option $vsobj $option] == 0 } {\n debug [list handle_opt_remove_on_redeploy $name] [format \"%s not available, skipping\" $option] 10\n return 0\n }\n\n if { [set [subst ::$name]] == $checkvalue && \\n [is_new_value $name] && \\n $::redeploy } {\n debug [list handle_opt_remove_on_redeploy] [format \"%s %s on redeploy, setting %s to none\" $name $checkvalue $option] 10\n set cmd [format \"ltm virtual %s/%s %s none\" $::app_path $vsname $option]\n debug [list handle_opt_remove_on_redeploy tmsh_modify] $cmd 1\n tmsh::modify $cmd\n return 1\n }\n return 0\n}\n\n# Check provisioning cache for whether a specified module is provisioned and at what levels\n# Adapted from original code including the F5 iApp TCL helper library\n# Input: $module = name of the module\n# Output: $level = integer representation of the provisioning level. See levels array below\nproc is_provisioned { module } {\n array set levels {\n none 0\n minimum 1\n nominal 2\n dedicated 3\n }\n\n if { [info exists ::provision_cache($module)] } {\n debug [list is_provisioned cache_hit] \"$module $::__provision_cache($module)\" 10\n return [expr { $levels($::provision_cache($module)) >= 1 }]\n } else {\n debug [list is_provisioned cache_miss] \"$module\" 10\n return -1\n }\n}\n\n# Load provisioning cache with module provisioning levels\n# Adapted from original code including the F5 iApp TCL helper library\nproc load_provisioned { } {\n array set levels {\n none 0\n minimum 1\n nominal 2\n dedicated 3\n }\n set obj [tmsh::get_config sys provision]\n foreach mod $obj {\n set modname [lindex $mod 2]\n set modlevel [lindex $mod 3]\n if { [llength $modlevel] == 2 } {\n set modlevel [lindex $modlevel 1]\n } else {\n set modlevel none\n }\n set ::provision_cache($modname) $modlevel\n debug [list load_provisioned cache_set] \"$modname $modlevel\" 10\n }\n}\n\n# Consume an APL table and return a list containing the values of the var specified in $key\n# Input: $table = the raw APL table\n# $key = the name of the variable to add to the return list\n# Output: $retlist = A list of strings\nproc single_column_table_to_list { table key } {\n set retlist {}\n foreach row $table {\n array unset column\n\n # extract the iApp table data - borrowed from f5.lbaas.tmpl\n foreach column_data [lrange [split [join $row] \"\n\"] 1 end-1] {\n set name [lindex $column_data 0]\n set column($name) [lrange $column_data 1 end]\n }\n if { [info exists column($key)] && [string length $column($key)] > 0 } {\n lappend retlist \"$column($key)\"\n }\n\n }\n return $retlist\n}\n\n# Process a string in the format node_cache for re-use\n# Input: string node_name = The object name to check for\n# Return: 0 = node does not exist\n# 1 = node does exist\nproc check_node_exist { node_name } {\n if { ! [info exists ::node_cache($node_name)] } {\n set node_status [catch {tmsh::get_config ltm node $node_name} node_status_ret]\n debug [list check_node_exist $node_name status] $node_status_ret 10\n if { [string match \"address\" $node_status_ret] } {\n set ::node_cache($node_name) 1\n debug [list check_node_exist $node_name cache_set] \"1\" 10\n return 1\n } else {\n set ::node_cache($node_name) 0\n debug [list check_node_exist $node_name cache_set] \"0\" 10\n return 0\n }\n } else {\n debug [list check_node_exist $node_name cache_hit] $::node_cache($node_name) 10\n return $::node_cache($node_name)\n }\n}\n\n# Perform string substitution on a URL.\n# Input: string url = the URL to manipulate\n# Output: string url = the final URL\nproc url_subst { url } {\n debug [list url_subst] [format \"url=%s\" $url] 10\n set url_map [list %APP_NAME% $::app \\n %APP_PATH% $::app_path \\n %PARTITION% $::partition \\n %VS_NAME% $::vs__Name \\n %VS_DESCR% $::vsDescription \\n %EXT1% $::extensionsField1 \\n %EXT2% $::extensionsField2 \\n %EXT3% $::extensionsField2 \\n \"url=\" \"\" \\n \"irule:url=\" \"\" \\n \"irule:urloptional=\" \"\" \\n \"asm:url=\" \"\" \\n \"apm:url=\" \"\" ]\n\n set url [string map $url_map $url]\n debug [list url_subst] [format \"return=%s\" $url] 10\n return $url\n}\n\n# Load a crypto cert/key from URL\n# Input: string type = key|cert\n# string url = the URL to get\n# Return: string obj_name = the name of the created TMOS object\nproc load_crypto_object { type url } {\n set url [url_subst $url]\n debug [list load_crypto_object url_subst] $url 10\n\n set url_file_name [lindex [split $url /] end]\n set objname [format \"/%s/%s%s\" $::partition $::app $url_file_name]\n set filename [format \"/var/tmp/appsvcs%s%s%s\" $::app $::bundler_timestamp $url_file_name]\n debug [list load_crypto_object] [format \"obj_name=%s file_name=%s\" $obj_name $file_name] 10\n\n switch -glob $type {\n cert { set verify_cmd [format \"/usr/bin/openssl x509 -noout -in %s\" $file_name] }\n key { set verify_cmd [format \"/usr/bin/openssl rsa -noout -in %s\" $file_name] }\n default { error \"The crypto type specified is not supported\" }\n }\n\n curl_save_file $url $file_name\n\n set verify_status [catch {eval exec $verify_cmd} verify_status_ret]\n debug [list load_crypto_object verify_status $verify_status] $verify_status_ret 10\n\n if { $verify_status } {\n file delete $file_name\n error \"While loading the $type: $verify_status_ret\"\n }\n\n set cmd [format \"sys file ssl-%s %s source-path file://%s\" $type $obj_name $file_name]\n debug [list load_crypto_object tmsh_create] $cmd 10\n set create_status [catch {tmsh::create $cmd} create_status_ret]\n debug [list load_crypto_object create_status $create_status] $create_status_ret 10\n file delete $file_name\n\n return $obj_name\n}\n\n# Credit: http://www.egghelp.org/cgi-bin/tcl_archive.tcl?mode=download&id=97\n# Perform a DNS lookup of a hostname using nslookup. Assumes DNS servers are already\n# configured in TMOS\n# Input: string host = name of host to lookup\n# int mode = 1 => Throw a hard error\n# 0 => Return the error\n# Return: string return = First IP tied to hostname OR Error\nproc dns_lookup { host {mode 1} } {\n set name \"Unknown\"\n set ip \"Unknown\"\n set errmsg \"Unknown\"\n set host [lindex [string tolower $host] 0]\n if {[catch {eval exec \"/usr/bin/nslookup\" [lindex $host 0]} buff]} {\n foreach line [split $buff \n] {\n if {[string first \"${host}:\" $line] != -1} {\n set errmsg [string trim [lindex [split $line :] 1]]\n }\n }\n\n if { $mode } {\n error \"An error occured trying to resolve $host: $errmsg\"\n } else {\n return \"Error: $errmsg\"\n }\n }\n set buff [split $buff \n]\n set buff [lreplace $buff 0 1]\n if {[regexp {name = (.*)\.} $buff -> name]} { set ip $host }\n\n foreach data $buff {\n switch [lindex $data 0] {\n \"Name:\" {\n set name [string trim [lindex [split $data :] 1]]\n }\n \"Address:\" {\n set ip [string trim [lindex [split $data :] 1]]\n }\n \"Addresses:\" {\n set ip [string trim [lindex [split $data :] 1]]\n }\n }\n }\n return \"${ip}\"\n}\n\nset_version_info\n\n# Copyright (c) 2017 F5 Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# ####################################################################\n# Custom extensions example\n#\n# The purpose of custom extensions is to allow functionality to be implemented\n# without modifying the base deployment code. Additionally control over these\n# extensions can be exposed via the extensionsfieldX fields to allow functionality\n# to be added WITHOUT changes to the presentation layer. By exposing the extension\n# fields as tenant editable we can add code to this portion of the iApp to handle\n# new functionality without changing the northbound data model\n#\n# The following procs are called at various points during the implementation:\n# custom_extensions_start: Called at the start of the deployment after mode is determined\n# custom_extensions_before_pools: Called before processing all pool(s) starts\n# custom_extensions_before_pool: Called before processing to create the pool starts\n# custom_extensions_after_pool: Called immediately after the pool is created\n# custom_extensions_after_pools: Called immediately after all pool(s) are created\n# custom_extensions_before_vs: Called before processing to create the virtual server starts\n# custom_extensions_after_vs: Called immediately after the virtual server is created\n# custom_extentionsend: Called at the end of the deployment\n#\n# Guidelines:\n# - Avoid name collisions please prefix variables with 'custom' unless used by the base deployment code\n# - Restrict modifications to global presentation layer variables unless absolutely required\n# - Try to modify the config once created by the base deployment code to maintain compatibility\n#\n# Two examples are implemented here:\n# - custom_example_1: Called from all hooks to dump some info to the debug log\n# - custom_example_2: (Disabled by default) Called at the end of the deployment to execute\n# a tmsh::create command\n\nproc custom_extensions_start {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Example 1: Parse a string of the format \"key1=val1;key2=val2;key3=val3\" and populate an array.\n # The we call the custom_example proc to dump some info to the /var/tmp/scriptd.out log\n\n # Make the global variable accessable locally. Additionally create a global array to store KVP pairs\n upvar extensionsField1 field1\n upvar custom_field1_kvp kvp_array\n\n # Check to see we got some data in extensionsField1\n if { [string length $field1] > 0 } {\n # Use the process_kvp_string proc to populate an array\n array set kvp_array [process_kvp_string $field1]\n\n debug \"[lindex [info level 0] 0]\" \"kvp_array=[array get kvp_array]\" 0\n # Call our custom_example_1 proc to dump some info to the debug log.\n custom_example_1 [array get kvp_array]\n }\n}\n\nproc custom_extensions_before_pools {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_before_pool {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_after_pool {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_after_pools {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_before_vs {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_after_vs {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n}\n\nproc custom_extensions_end {} {\n debug \"[lindex [info level 0] 0]\" \"entering proc\" 6\n\n # Call our custom_example_1 proc to dump some info to the debug log.\n upvar custom_field1_kvp kvp_array\n custom_example_1 [array get kvp_array]\n\n # Call our custom_example_2 proc to run a user provided tmsh create command\n #\n # Populate extensionsField2 with a valid command like:\n # ltm data-group internal customDG type string records replace-all-with { record1 { data data1 } record2 { data data2 } }\n #\n # Once the template executes you can see the creation of the datagroup under the application template container\n\n # To enable example 2 uncomment the following two lines \n # upvar extensionsField2 field2\n # custom_example_2 $field2\n}\n\n# Example 1: Simply dump a log line to /var/tmp/scriptd.out\nproc custom_example_1 { kvp_array_in } {\n set calling_proc [lindex [info level -1] 0]\n set current_proc [lindex [info level 0] 0]\n array set kvp_array $kvp_array_in\n\n debug [list $current_proc] \"entering proc kvp_array_in=$kvp_array_in\" 6\n\n if { [info exists kvp_array(custom_example)] && $kvp_array(custom_example) == 1} {\n debug [list $current_proc] \"This is an example of a custom extension called from $calling_proc\" 6\n }\n}\n\n# Example 2: Run the text in extensionsField2 as a tmsh create command\nproc custom_example_2 { cmd } {\n set calling_proc [lindex [info level -1] 0]\n set current_proc [lindex [info level 0] 0]\n\n debug [list $current_proc] \"entering proc cmd=$cmd\" 6\n\n if { [string length $cmd] > 0 } {\n debug [list $current_proc] \"Called from $calling_proc - About the execute tmsh::create $cmd\" 6\n tmsh::create $cmd\n }\n}\n\narray set bundler_objects {}\narray set bundler_data {}\nset bundler_deferred_cmds []\n\n\n\nset app $tmsh::app_name\ndebug [list start] [format \"Starting %s version IMPL=%s TMPLNAME=%s app_name=%s\" $NAME $IMPLVERSION $TMPLNAME $app] 0\ndebug [list version_info] [array get version_info] 3\n\narray set modenames {\n 1 {Standalone}\n 2 {iWorkflow}\n 3 {Cisco APIC}\n 4 {VMware NSX}\n}\n\narray set __provision_cache {}\narray set node_cache {}\nload_provisioned\n\narray set aso_config {}\nset asoobj {}\nset modeinfo [get_mode]\nset mode [lindex $modeinfo 0]\nset folder [lindex $modeinfo 1]\nset partition [lindex $modeinfo 2]\nset rd [lindex $modeinfo 3]\nset newdeploy [lindex $modeinfo 4]\nset app_path [format \"/%s/%s.app\" $partition $app]\nset template_name [format \"appsvcs_integration_v%s\" $IMPLMAJORVERSION]\nset aso [format \"%s/%s\" $app_path $app]\nset redeploy 0\nif { ! $newdeploy } { set redeploy 1 }\nset postfinal_deferred_cmds []\n\ndebug \"modeinfo\" [format \"mode=%s folder=%s partition=%s rd=%s newdeploy=%s redeploy=%s template_name=%s\" $mode $folder $partition $rd $newdeploy $redeploy $template_name] 2\n\n# Cache our ASO object data\nfor { set i 0 } { $i <= [llength $asoobj] } { set i [expr {$i+2}] } {\n set type [lindex $asoobj $i]\n if { $type == \"variables\" || $type == \"lists\" || $type == \"tables\" } {\n for { set j 0 } { $j < [llength [lindex $asoobj [expr {$i+1}]]] } { set j [expr {$j+2}] } {\n set name [lindex [lindex $asoobj [expr {$i+1}]] $j]\n\n if { $type == \"tables\" } {\n set val [lindex [lindex $asoobj [expr {$i+1}]] [expr {$j+1}]]\n } elseif { $type == \"lists\" } {\n set val [lindex [lindex [lindex $asoobj [expr {$i+1}]] [expr {$j+1}]] 1]\n } else {\n set val [lindex [lindex [lindex $asoobj [expr {$i+1}]] [expr {$j+1}]] 1]\n }\n set aso_config($name) $val\n }\n }\n}\n# Copy the array so that we can preserve the original values\n# aso_config() can change at runtime\narray set aso_config_orig [array get aso_config]\n\ndebug [list aso_config] [array names aso_config] 9\nforeach var [array names aso_config] {\n debug [list aso_config $var] $aso_config($var) 9\n}\n\nset asodescr [format \"Deployed by appsvcs_integration_v%s in %s mode on %s\" $IMPLVERSION $modenames($mode) [clock format $startTime -format \"%D %H:%M:%S\"]]\ndebug [list set_aso_decription tmsh_modify] [format \"sys application service %s description \\"%s\\"\" $aso $asodescr] 1\ntmsh::modify [format \"sys application service %s description \\"%s\\"\" $aso $asodescr]\n\n# Define various global values\nset allVars {\n iappstrictUpdates \\n iappappStats \\n iappmode \\n iapplogLevel \\n iapprouteDomain \\n iappasmDeployMode \\n iappapmDeployMode \\n pooladdr \\n poolmask \\n poolport \\n poolDefaultPoolIndex \\n poolPools \\n poolMemberDefaultPort \\n poolMembers \\n monitorMonitors \\n vsListeners \\n vsName \\n vsDescription \\n vsRouteAdv \\n vsSourceAddress \\n vsIpProtocol \\n vsConnectionLimit \\n vsProfileClientProtocol \\n vsProfileServerProtocol \\n vsProfileHTTP \\n vsProfileOneConnect \\n vsProfileCompression \\n vsProfileAnalytics \\n vsProfileRequestLogging \\n vsProfileDefaultPersist \\n vsProfileFallbackPersist \\n vsSNATConfig \\n vsProfileServerSSL \\n vsProfileClientSSL \\n vsProfileClientSSLCert \\n vsProfileClientSSLKey \\n vsProfileClientSSLChain \\n vsProfileClientSSLCipherString \\n vsProfileClientSSLAdvOptions \\n vsProfileSecurityLogProfiles \\n vsProfileSecurityIPBlacklist \\n vsProfileSecurityDoS \\n vsProfileAccess \\n vsProfileConnectivity \\n vsProfilePerRequest \\n vsOptionSourcePort \\n vsOptionConnectionMirroring \\n vsIrules \\n vsBundledItems \\n vsAdvOptions \\n vsAdvProfiles \\n vsAdvPolicies \\n vsVirtualAddrAdvOptions \\n l7policystrategy \\n l7policydefaultASM \\n l7policydefaultL7DOS \\n l7policyrulesMatch \\n l7policyrulesAction \\n featurestatsTLS \\n featurestatsHTTP \\n featureinsertXForwardedFor \\n featureredirectToHTTPS \\n featuresslEasyCipher \\n featuresecurityEnableHSTS \\n featureeasyL4Firewall \\n featureeasyL4FirewallBlacklist \\n featureeasyL4FirewallSourceList \\n extensionsField1 \\n extensionsField2 \\n extensionsField3 \\n}\n\nset requiredVars {\n pooladdr \\n poolmask \\n poolport \\n vsProfileClientProtocol }\n\narray set table_defaults {\n Members {\n Index 0\n State enabled\n IPAddress \"\"\n Port \"\"\n ConnectionLimit 0\n Ratio 1\n PriorityGroup 0\n AdvOptions \"\"\n }\n Pools {\n Index -1\n Name \"\"\n Description \"\"\n LbMethod \"\"\n Monitor \"\"\n AdvOptions \"\"\n }\n Monitors {\n Index -1\n Name \"\"\n Type \"\"\n Options \"\"\n }\n L7P_Match {\n Group -1\n Operand \"\"\n Negate no\n Condition \"\"\n CaseSensitive no\n Value \"\"\n Missing no\n }\n L7P_Action {\n Group -1\n Target error/error/error\n Parameter error\n }\n Listeners {\n Listener \"\"\n Destination \"\"\n }\n}\n\narray set pool_member_state {\n enabled {session user-enabled state user-up}\n disabled {session user-disabled state user-up}\n force-disabled {session user-disabled state user-down}\n drain-disabled {session user-disabled state user-up}\n}\n\n# Fixup incoming variables: If no value is sent for a particular iApp field than the var is not created which\n# results in all sorts of problems. We just check for existence of the var and set to \"\" if it doesn't exist\nforeach var $allVars {\n if {[info exists [subst $var]]} {\n debug \"input\" [format \"%s sent, value is: %s\" $var [set [subst $var]]] 2\n } else {\n set [subst $var] \"\"\n debug \"input\" [format \"%s NOT sent, setting to blank\" $var] 2\n }\n}\n\n# Double check we got all the required variables.\nforeach var $requiredVars {\n debug [list required_check] [format \"var=%s val=%s len=%s\" $var [set [subst $var]] [string length [set [subst $var]]]] 10\n if {! [info exists [subst $var]] || [string length [set [subst $var]]] == 0 } {\n if { ! [string match \"vs\" $var] && $pooladdr != \"255.255.255.254\" } {\n error \"The variable $var is required\"\n }\n }\n}\n\n# Convert the $vsBundledItems table to a list for easier manipulation\nset vsBundledItems [single_column_table_to_list $vsBundledItems \"Resource\"]\ndebug [list convert_bundled] $vsBundledItems 7\n\n# Call the custom_extensions_start proc to allow site-specific customizations\ncustom_extensions_start\n\n# Special handling for the Source Address because it comes in as 0.0.0.0/0 and\n# needs to be 0.0.0.0%xxxx/0, where '%xxxx' is the route-domain ID\nset working $vsSourceAddress\ndebug [list fix_src_addr] \"Check if vsSourceAddress needs to be fixed\" 7\nif { [string length $working] > 0 } {\n set net [lindex [split $working /] 0]\n set cidr [lindex [split $working /] 1]\n set vsSourceAddress \"$net%$rd\/$cidr\"\n debug [list fix_src_addr] [format \" Fixing vsSourceAddress: orig=%s new=%s\" $working $vsSourceAddress] 7\n}\n\n# Create Client-SSL profile if Cert and Key are specified but ClientSSLProfile is not\ndebug [list client_ssl create] \"checking if client ssl cert & key were entered\" 7\nset clientssl 0\nif { [string length $vsProfileClientSSLKey] > 0 && [string length $vsProfileClientSSLCert] > 0 && [string length $vsProfileClientSSL] == 0 } {\n\n set crypto_url_found 0\n if { [string match \"url=\" $vsProfileClientSSLKey] } {\n set vsProfileClientSSLKey [load_crypto_object \"key\" $vs__ProfileClientSSLKey]\n set crypto_url_found 1\n }\n\n if { [string match \"url=\" $vsProfileClientSSLCert] } {\n set vsProfileClientSSLCert [load_crypto_object \"cert\" $vsProfileClientSSLCert]\n set crypto_url_found 1\n }\n\n if { [string match \"url=*\" $vsProfileClientSSLChain] } {\n set vsProfileClientSSLChain [load_crypto_object \"cert\" $vsProfileClientSSLChain]\n set crypto_url_found 1\n }\n\n if { $vsProfileClientSSLKey == \"auto\" } {\n debug [list client_ssl create auto_key] [format \"found auto option for key, setting vsProfileClientSSLKey=/Common/%s.key\" $app] 5\n set vsProfileClientSSLKey \"/Common/$app.key\"\n }\n\n if { $vsProfileClientSSLCert == \"auto\" } {\n debug [list client_ssl create auto_cert] [format \"found auto option for key, setting vsProfileClientSSLCert=/Common/%s.crt\" $app] 5\n set vsProfileClientSSLCert \"/Common/$app.crt\"\n }\n\n if { $crypto_url_found == 0 } {\n tmsh::get_config /sys file ssl-key $vsProfileClientSSLKey\n tmsh::get_config /sys file ssl-cert $vs__ProfileClientSSLCert\n debug [list client_ssl create check_exist] \"ssl cert & key found... creating profile\" 7\n }\n\n set cmd [format \"ltm profile client-ssl %s_clientssl key %s cert %s\" $app $vsProfileClientSSLKey $vsProfileClientSSLCert]\n\n if { [string length $vsProfileClientSSLChain] > 0 } {\n if { $crypto_url_found == 0 } {\n tmsh::get_config /sys file ssl-cert $vsProfileClientSSLChain\n }\n debug [list client_ssl create cert_chain] \"adding cert chain\" 7\n append cmd [format \" chain %s\" $vsProfileClientSSLChain]\n }\n\n\tarray set feature_sslEasyCipher_strings {\n\t\tcompatible {NATIVE:!SSLv3:!SSLv2:!EXPORT:!MD5:!ADH:@STRENGTH}\n\t\tmedium {TLSv1_2+HIGH:TLSv1_1+HIGH:TLSv1+MEDIUM:TLSv1+HIGH:!EXPORT:!RC4:!EXPORT:!MD5:!ADH:@STRENGTH}\n\t\thigh {TLSv1_2+HIGH:TLSv1_1+HIGH:TLSv1+MEDIUM:TLSv1+HIGH:!RC4:!RSA:!DHE:!EXPORT:!MD5:!ADH:@STRENGTH}\n\t\ttls_1.2 {TLSv1_2:!TLSv1_2+LOW:!EXPORT:!MD5:!ADH:@STRENGTH}\n\t\ttls_1.1+1.2 {TLSv1_2:TLSv1_1:!TLSv1_2+LOW:!TLSv1_1+LOW:!EXPORT:!MD5:!ADH:@STRENGTH}\n\t}\n\n\tif { $featuresslEasyCipher ne \"disabled\" && [info exists feature_sslEasyCipher_strings($featuresslEasyCipher)]} {\n\t\tdebug [list client_ssl create ssl_easy_cipher] [format \"sslEasyCipher is not disabled, setting vsProfileClientSSLCipherString=%s\" $feature_sslEasyCipher_strings($featuresslEasyCipher)] 5\n\t\tset vsProfileClientSSLCipherString $feature_sslEasyCipher_strings($featuresslEasyCipher)\n\t}\n\n if { [string length $vsProfileClientSSLCipherString] > 0 } {\n debug [list client_ssl create cipher_string] \"adding cipher string\" 7\n append cmd [format \" ciphers \\"%s\\"\" $vsProfileClientSSLCipherString]\n }\n\n if { [string length $vsProfileClientSSLAdvOptions] > 0 } {\n debug [list client_ssl create adv_options] \"processing advanced options string\" 7\n append cmd [format \" %s\" [process_options_string $vsProfileClientSSLAdvOptions \"profile client-ssl\" \"/Common/clientssl\"]]\n }\n\n debug [list client_ssl create tmsh_create] $cmd 1\n tmsh::create $cmd\n set clientssl 1\n} else {\n if { [string length $vsProfileClientSSL] > 0 } {\n if { ![string match \"create:*\" $vsProfileClientSSL] } {\n debug [list client_ssl associate] \"ClientSSLProfile was provided... checking if it exists\" 5\n tmsh::get_config /ltm profile client-ssl $vsProfileClientSSL\n set clientssl 2\n } else {\n debug [list client_ssl create] \"create ClientSSLProfile was provided...\" 5\n set clientssl 3\n }\n } else {\n set clientssl 0\n if { [string length $vsProfileClientSSLKey] > 0 && [string length $vsProfileClientSSLCert] == 0 } {\n error \"A client-ssl key was specified without a client-ssl certifcate\"\n }\n if { [string length $vsProfileClientSSLKey] == 0 && [string length $vsProfileClientSSLCert] > 0 } {\n error \"A client-ssl certifcate was specified without a client-ssl key\"\n }\n debug [list client_ssl] \"ssl cert & key not specified... skipped Client-SSL profile creation\" 2\n }\n}\n\n# Fixup empty poolPools and monitorMonitors table if poolMembers is populated\n# The behaviour implemented here will create a pool with a round-robin lb-method\n#\n# If a monitor with Index 0 is present in the monitorMonitors table\n# the pool monitor will be set to that.\n#\n# If the monitorMonitors is empty a default monitor will be created\nset monCount [llength $monitorMonitors]\nset poolCount [llength $poolPools]\nset poolMemberCount [llength $poolMembers]\n\nset poolTmpl {{{\n AdvOptions none\n Description\n Index %INDEX%\n LbMethod round-robin\n Monitor %MONITOR%\n Name\n}} }\n\nset monitorTmpl {{{\n Index 0\n Name %NAME%\n Type\n Options\n}} }\n\nif { $poolCount == 0 && $poolMemberCount > 0 } {\n debug [list pools_fixup] [format \"poolCount=%s\" $poolCount] 7\n debug [list pools_fixup] [format \"monCount=%s\" $monCount] 7\n debug [list pools_fixup] [format \"poolMemberCount=%s\" $poolMemberCount] 7\n set poolFixupIndexes []\n array set poolFixupFound {}\n foreach memberRow $poolMembers {\n array unset memberColumn\n array set memberColumn {}\n table_row_to_array $memberRow memberColumn ::table_defaults(Members) [list AdvOptions]\n if { ![info exists poolFixupFound($memberColumn(Index))] } {\n debug [list pools_fixup found_pool] [format \"index=%s\" $memberColumn(Index)] 7\n lappend poolFixupIndexes $memberColumn(Index)\n set poolFixupFound($memberColumn(Index)) 1\n }\n }\n debug [list pools_fixup create_indexes] $poolFixupIndexes 7\n\n set poolFixupMonitor \"0\"\n if { $monCount == 0 } {\n if { $vsIpProtocol == \"tcp\" } {\n if { [string length $vs__ProfileHTTP] > 0 } {\n set monFixupName \"/Common/http\"\n } else {\n set monFixupName \"/Common/tcp\"\n }\n set monTmpl_map [list %NAME% $monFixupName]\n set monitorMonitors [string map $monTmpl_map $monitorTmpl]\n debug [list pools_fixup monitorMonitors] $monitorMonitors 7\n } else {\n set poolFixupMonitor \"\"\n }\n }\n\n set poolTemp \"\"\n foreach foundIndex $poolFixupIndexes {\n set poolTmpl_map [list %INDEX% $foundIndex \\n %MONITOR% $poolFixupMonitor ]\n append poolTemp [string map $poolTmpl_map $poolTmpl]\n }\n set poolPools $poolTemp\n debug [list pools_fixup poolPools] $poolPools 7\n\n set monCount [llength $monitorMonitors]\n set poolCount [llength $poolPools]\n}\n\n# Create Monitors\ndebug [list monitors] [format \"monCount=%s\" $monCount] 7\nset monIdx 0\narray set monNames {}\narray set monCreate {}\nforeach monRow $monitor__Monitors {\n set cmd \"\"\n debug [list monitors $monIdx] [format \"monRow=%s\" $monRow] 9\n\n array unset monColumn\n array set monColumn {}\n table_row_to_array $monRow monColumn ::table_defaults(Monitors)\n debug [list monitors table_row_to_array return] [array get monColumn] 7\n\n # Fixup the Index in case a table with exactly one row and no Index is sent\n if { [llength $monitorMonitors] == 1 && $monColumn(Index) == -1 } {\n debug [list monitors fixup_index] \"setting Index to 0\" 9\n set monColumn(Index) 0\n }\n\n # The BIG-IP UI sends empty rows... above this we set Index to -1 if it wasn't found\n # If a Index is not specified then skip this row in the table\n if { $monColumn(Index) < 0 } {\n debug [list monitors $monIdx check_index] \"no index value found, skipping row\" 9\n continue\n } elseif { [info exists monNames($monColumn(Index))] } {\n error \"A monitor with Index of \\"$monColumn(Index)\\" was already specified\"\n } else {\n if {[string length $monColumn(Name)] > 0 } {\n if { [string match \"/\" $monColumn(Name)] } {\n set monNames($monColumn(Index)) $monColumn(Name)\n set monCreate($monColumn(Index)) 0\n } else {\n set monNames($monColumn(Index)) [format \"%s/%s\" $apppath $monColumn(Name)]\n set monCreate($monColumn(Index)) 1\n }\n } else {\n set monNames($monColumn(Index)) [format \"%s/monitor%s\" $app_path $monColumn(Index)]\n set monCreate($monColumn(Index)) 1\n }\n }\n\n if { $monCreate($monColumn(Index)) == 1 } {\n if { [string length $monColumn(Type)] <= 0 } {\n error \"A Monitor Type was not specified for monitor with Index $monColumn(Index)\"\n }\n\n set cmd [format \"ltm monitor %s %s \" $monColumn(Type) $monNames($monIdx)]\n if { [string length $monColumn(Options)] > 0 } {\n set monColumn(Options) [join $monColumn(Options) \" \"]\n debug [list monitors $monIdx options] [format \"processing options string \\"%s\\"\" $monColumn(Options)] 10\n append cmd [format \" %s\" [process_options_string $monColumn(Options) \"\" \"\"]]\n }\n\n debug [list monitors $monIdx tmsh_create] $cmd 1\n tmsh::create $cmd\n }\n\n incr monIdx\n}\n\n# Call the custom_extensions_before_pool proc to allow site-specific customizations\ncustom_extensions_before_pools\n\n# Create pool\ndebug [list pools] [format \"poolCount=%s\" $poolCount] 7\n\nset poolIdx 0\nset default_pool_name \"\"\narray set poolIndexes {}\narray set poolNames {}\nforeach poolRow $poolPools {\n set cmd \"\"\n set numMembers 0\n\n debug [list pools $poolIdx] [format \"poolRow=%s\" $poolRow] 9\n\n custom_extensions_before_pool\n\n array unset poolColumn\n array set poolColumn {}\n table_row_to_array $poolRow poolColumn ::table_defaults(Pools) [list AdvOptions]\n debug [list pools $poolIdx table_row_to_array return] [array get poolColumn] 7\n\n # Fixup the Index in case a table with exactly one row and no Index is sent\n if { [llength $poolPools] == 1 && $poolColumn(Index) == -1 } {\n debug [list pools $poolIdx fixup_index] \"setting Index to 0\" 9\n set poolColumn(Index) 0\n }\n\n # The BIG-IP UI sends empty rows... above this we set Index to -1 if it wasn't found\n # If a Index is not specified then skip this row in the table\n if { $poolColumn(Index) < 0 } {\n debug [list pools $poolIdx] \"no index value found, skipping row\" 9\n continue\n } elseif { [info exists poolIndexes($poolColumn(Index))] } {\n error \"A pool with Index of \\"$poolColumn(Index)\\" was already specified\"\n } else {\n set poolIndexes($poolColumn(Index)) 1\n }\n\n # Check to see if a poolName was specified... if not set to $apppool$poolColumn(Index)\n if { [string length $poolColumn(Name)] == 0 } {\n set poolColumn(Name) [format \"%spool%s\" $app $poolColumn(Index)]\n debug [list pools $poolIdx] [format \"no pool name specified... setting to %s\" $poolColumn(Name)] 7\n }\n set poolNames($poolColumn(Index)) $poolColumn(Name)\n\n if { $poolColumn(Index) == $poolDefaultPoolIndex } {\n # Set the default pool name for use later during virtual server creation\n set default_pool_name $poolColumn(Name)\n }\n\n # Process the poolMembers table\n set memberStr \"members replace-all-with \{ \"\n foreach memberRow $poolMembers {\n array unset memberColumn\n array set memberColumn {}\n table_row_to_array $memberRow memberColumn ::table_defaults(Members) [list AdvOptions]\n\n set memberId [format \"%s/%s:%s\" $memberColumn(Index) $memberColumn(IPAddress) $memberColumn(Port)]\n\n if { [llength $poolPools] == 1 && $memberColumn(Index) == -1 } {\n set memberColumn(Index) 0\n }\n\n if { $memberColumn(Index) != $poolColumn(Index) } {\n debug [list pools $poolIdx members $memberId skip_index] [format \"not a member of pool %s skipping\" $poolColumn(Index)] 11\n continue\n }\n\n debug [list pools $poolIdx members $memberId config_raw] [array get memberColumn] 9\n\n\n set memberColumn(AdvOptions) [lindex $memberColumn(AdvOptions) 0]\n\n # We support for many option formats for the IPAddress field. Examples are:\n # 0.0.0.0 Special value that signals to skip this pool member\n # x.x.x.x[%y][;nodename] IPv4 Address w/w/o Route Domain or Node Name\n # abcd::0001[%y][;nodename] IPv6 Address w/w/o Route Domain or Node Name\n # /Common/node_name Pre-existing node name\n # node_name Pre-existing node name without folder (default folder=Common)\n # hostname.org.com A DNS Hostname (resolved on deployment)\n #\n # These options are processed as follows:\n # 1) Skip row if IPAddress == \"0.0.0.0\" or is empty\n # 2) Determine if a node object was specified or not\n # 3) If node object does not exist process as follows\n # 3a) If a nodename option was specified create the node object\n # 3b) If not IP than assume a hostname and resolve IP, create node using hostname, add node to member string\n\n # 1) Skip pool members with a 0.0.0.0 or empty IP. Added to allow creation of an empty pool when you still have\n # to expose the pool member IP as a tenant editable field in iWorkflow (Cisco APIC needs this for Dynamic Endpoint Insertion)\n if { [string match 0.0.0.0 $memberColumn(IPAddress)] || [string length $memberColumn(IPAddress)] == 0 } {\n debug [list pools $poolIdx members $memberId skip_ip] \"ip=0.0.0.0 or empty, skipping\" 7\n continue\n } else {\n incr numMembers\n }\n\n # TODO: Is this still required?\n # Sometimes we receive a transposed ip/port from iWorkflow... fix it here\n if {[has_routedomain $memberColumn(Port)]} {\n set new_port $memberColumn(IPAddress)\n set new_ip $memberColumn(Port)\n set memberColumn(Port) $new_port\n set memberColumn(IPAddress) $new_ip\n debug [list pools $poolIdx members $memberId fix_ip_port] [format \"ip=%s port=%s\" $memberColumn(IPAddress) $memberColumn(Port)] 7\n }\n\n set node_default_folder \"/Common/\"\n if { [string first \"/\" $memberColumn(IPAddress)] >= 0 } { set node_default_folder \"\" }\n set node_create 0\n if { [string first \; $memberColumn(IPAddress)] > 1 } {\n set memberColumn(IPAddress) [lindex $memberColumn(IPAddress) 0]\n set node_info [psplit $memberColumn(IPAddress) \;]\n set memberColumn(IPAddress) [lindex $node_info 0]\n set memberColumn(NodeName) [lindex $node_info 1]\n set node_obj_name [format \"%s%s\" $node_default_folder $memberColumn(NodeName)]\n debug [list pools $poolIdx members $memberId named_node node_obj_name] $node_obj_name 7\n } else {\n set node_obj_name [format \"%s%s\" $node_default_folder $memberColumn(IPAddress)]\n debug [list pools $poolIdx members $memberId node_obj_name] $node_obj_name 7\n }\n\n # 2) Determine if a node object was specified rather than an IP address\n set node_exist [check_node_exist $node_obj_name]\n debug [list pools $poolIdx members $memberId set_blank_folder] \"folder=$node_default_folder name=$node_obj_name exist=$node_exist\" 7\n\n debug [list pools $poolIdx members $memberId is_ip] \"$memberColumn(IPAddress) [is_ip $memberColumn(IPAddress)]\" 7\n\n if { [info exists memberColumn(NodeName)] && $node_exist == 0 } {\n set node_create 1\n } else {\n if { $node_exist == 0 && [is_ip $memberColumn(IPAddress)] == 0 } {\n set memberColumn(NodeName) $memberColumn(IPAddress)\n set memberColumn(IPAddress) [dns_lookup $memberColumn(IPAddress)]\n debug [list pools $poolIdx members $memberId resolved_ip] $memberColumn(IPAddress) 7\n set node_create 1\n }\n }\n\n if { $node_create } {\n set node_cmd [format \"ltm node /%s/%s address %s\" $partition $memberColumn(NodeName) $memberColumn(IPAddress)]\n debug [list pools $poolIdx members $memberId named_node tmsh_create] $node_cmd 7\n tmsh::create $node_cmd\n set node_cache($memberColumn(NodeName)) 1\n set node_exist 1\n set memberColumn(IPAddress) [format \"/Common/%s\" $memberColumn(NodeName)]\n }\n\n # Add a route domain if it wasn't included and we don't already have a node object created\n if { $node_exist == 0 && ![has_routedomain $memberColumn(IPAddress)]} {\n set memberColumn(IPAddress) [get_dest_addr $memberColumn(IPAddress)]\n }\n\n # If we don't get a port in the pool member table than use the template value for poolMemberDefaultPort\n # If poolMemberDefaultPort is empty than use the value for poolport\n if { [string length $memberColumn(Port)] == 0} {\n if { [string length $poolMemberDefaultPort] == 0 } {\n debug [list pools $poolIdx members $memberId port_sub_vs] [format \"using %s\" $poolport] 5\n set memberColumn(Port) $poolport\n } else {\n debug [list pools $poolIdx members $memberId port_sub_default] [format \"using %s\" $poolMemberDefaultPort] 5\n set memberColumn(Port) $pool__MemberDefaultPort\n }\n }\n\n debug [list pools $poolIdx members $memberId normalized_config] [array get memberColumn] 7\n\n if { [string length $memberColumn(AdvOptions)] > 0} {\n debug [list pools $poolIdx members $memberId adv_options] \"processing member advanced options string\" 7\n set memberColumn(AdvOptions) [format \" %s\" [process_options_string $memberColumn(AdvOptions) \"\" \"\"]]\n }\n\n if { $node_exist } {\n # Node did exist, create
@caphrim007 I am not sure If I posted that in a way that is usable. If the above is too much of a mess let me know and I will give it another try to have it formatted better.
@tthomas0702 this gets the point across.
Here's my point. You mention that you are able to post the entire template using postman. The fact of the matter is that you posted a single template, and that template is special because nowhere in it are fields such as "presentation", "macro", "html-help", "role-acl", etc.
Your template there is only an implementation
.
An iApp template has a loose specification, but a basic set of fields that it can take. For example, see this stub.
You have provided only a single one of those fields, implementation
. Were your iApp to have any number of other fields, you would not be able to give the iApp to the REST api in the manner that you did. A presentation
field cannot go in an implementation
field.
This is the crux of the problem with the REST API for iApp templates and the reason that I suggest you avoid using it entirely. The iApps that F5 releases contain all those fields in that example and more. Some of the additional fields are not even settable via the iApp template API and need to be set at other APIS (such as procs).
If you have one of those iapps, then the burden of parsing this file (such as this https://github.com/F5Networks/f5-ansible/blob/devel/test/integration/targets/bigip_iapp_template/files/f5.microsoft_exchange_2016.v1.0.0.tmpl) is entirely on your shoulders. Because the valid REST representation of that iApp is
tm.sys.application.templates_template.create(
implementation=YOUR_PARSED_IMPLEMENTATION_BLOCK,
presentation=YOUR_PARSED_PRESENTATION_BLOCK,
htmlHelp=YOUR_PARSED_HTML_HELP_BLOCK,
macros=YOUR_PARSED_MACROS_BLOCK
)
as well as 10's or more API calls to create each proc at the expected APIs.
This is hard and no customer would ever expect to have to do this...I mean...F5 gave me this iApp, why do I need to parse it? That's crazy talk to expect that. And yet, this is the API.
So to mitigate that problem, the approach I recommend is to not use the API at all at this time. Instead, use the method of put the iApp on the box via an upload, and then use tmsh over REST to install it. At least until such time as a new API exists that behaves in a similar manner (or they fix the existing one because it is, arguably, unusable).
@caphrim007 OK, I understand better now.
Thanks for explaining. I think I was a little confused by the fact that it works when I POST it. The appscvs iApp must be a special case. I will stop trying to import an iApp and move on to deploying an iApp.
closing as answered
I am attempting to import an iApp to a BIG-IP and failing. I am not 100% sure I am doing it correctly so I would like to show how I am doing it and then show the data I collected on the results. I can import an iApp template successfully using Postman.
If I am doing this wrong please give me some pointers and ignore the data I collected about the failure.
Versions: Python 2.7.12 f5-sdk 3.0.8 BIG-IP 13.0.0-HF3
What I am dong that is failing:
Question: For "template" should I be giving JSON or iApp tcl script? In the above I am giving json.
I took capture durng a failure and during a working example with Postman to see the difference. Some parts have been shorted to make more readable:
Failure:
Working Postman Capture:
It looks like, in the failed capture, that some keys are being added into the json that look out of place. eg. "partition", "name", and "template" at the begining. If I leave out "partition" or "name", it errors requiring them.
I have tried a number of modifications to the json but always get the same error. There error message is originating from the BIG-IP:
/var/log/ltm: Jan 7 14:17:48 ip-10-0-3-235 err mcpd[5763]: 01070734:3: Configuration error: An application template (/Common/appsvcs_integration_v2.0.004) must define a "definition" action
I attached the file with the POST data I am using: json_post_appsvcs_2.0.004.txt