#!/usr/bin/perl -w # # tmsh2iapp.pl - A tool for easily generating an iApp from an existing TMSH configuration using a TMSH-like template # # The tool is also able to generate a HEAT template to instantiate this iApp in Openstack # ########################################################################################################################### # Copyright 2016-2018 F5 Networks Inc # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ########################################################################################################################### # # changelog: # # Ulises Alonso Camaro # # 2016/06/10 - u.alonsocamaro@f5.com - Initial version # 2016/06/13 - u.alonsocamaro@f5.com - Embedding of TMSH configuration in the iApp and initial documentation # 2016/06/14 - u.alonsocamaro@f5.com - FIX: when there is only one pool defined # 2016/06/14 - u.alonsocamaro@f5.com - warn when pools are defined when using the "system" option # 2016/06/14 - u.alonsocamaro@f5.com - FIX: do not create var section when there are no variables # 2016/06/14 - u.alonsocamaro@f5.com - strings are now displayed with the xxlarge display hint # 2016/06/23 - u.alonsocamaro@f5.com - Force the customer to use a full path when specifying the template # 2016/06/24 - u.alonsocamaro@f5.com - Optionally allow labels and properties to variables, pool members and internal data-group records # 2016/06/24 - u.alonsocamaro@f5.com - Allow dynamic number of records for internal data-groups # 2016/06/24 - u.alonsocamaro@f5.com - pool_members_modify and data_records_modify now add code in the iApp to only modify the pool/data-group if the there are pool members/data records # 2016/06/24 - u.alonsocamaro@f5.com - FIX: allow templates which contain fields with double quotes in them # 2016/07/22 - u.alonsocamaro@f5.com - Removed app-service keyword from resulting iApp # 2016/07/22 - u.alonsocamaro@f5.com - Introduced disable strict updates. Allows the generated iApp by modified. I needed this for a use-case. # Note: This is a workaround until HEAT teampltes allow deploying iApps with strict updates disabled. # 2016/09/20 - u.alonsocamaro@f5.com - Make conditional "disable strict updates" by making it a parameter. Also found that it doesn't work for "system" iApps so this is also a FIX. # 2016/09/20 - u.alonsocamaro@f5.com - Usability improvements (hopefully :-): # + Full path of the template is no longer needed # + Now the resulting .cfg file which is load at iapp instantiation is stored in /var/tmp, this can be changed with the cfgdir variable # + Now the template file (input file of this utility) needs to have .tmpl extension # + Now the resulting iapp is not written to stdout but instead to a file in the same folder as the template but with the .iapp extension. # 2016/11/30 - u.alonsocamaro@f5.com - Added common and common-disable-strict-updates to allow creation of iApps that have objects in the /Common partition # # 2016/12/01 - u.alonsocamaro@f5.com - HEAT template creation preparation: iApp creation functions renamed with iapp_ prefix # 2016/12/01 - u.alonsocamaro@f5.com - HEAT template creation preparation: function relocation # 2016/12/01 - u.alonsocamaro@f5.com - HEAT template creation introduced # 2016/12/07 - u.alonsocamaro@f5.com - Now the input file of this tool must have .t2i extension instead of .tmpl to avoid confusion because .tmpl is sometimes used with iApps. # The iApps generated by this tool will continue having .iapp extension because it is unambiguous. # 2017/04/18 - u.alonsocamaro@f5.com - Initial support for firewall address and port lists. heat-create needs to be updated. # 2017/04/20 - u.alonsocamaro@f5.com - Assign empty strings to variables if these are not defined and assign them {} for proper substitution by string map # 2017/04/24 - u.alonsocamaro@f5.com - Allow regular variables that have values separated by spaces # 2017/04/28 - u.alonsocamaro@f5.com - Previous change makes the init_variables code not working well with empty values because they now become {}. That previous change on 2017/04/20 seems not useful any more. Commented out. # 2017/05/23 - u.alonsocamaro@f5.com - Initial support for PEM URL categories in policy rules # 2017/05/24 - u.alonsocamaro@f5.com - Some objects in the .t2i need to reference the iApp's instance folder, we make this folder available through a @service_folder token. This is only available when using the service and service-disable-strict-updates options. # 2017/05/25 - u.alonsocamaro@f5.com - Introduction of @vartypes for simplifyling the script and making easier to extend for new var types # 2017/05/25 - u.alonsocamaro@f5.com - Re-implemented heat-create using @vartypes # 2017/05/25 - u.alonsocamaro@f5.com - Implementation of ansible-create # 2017/06/09 - u.alonsocamaro@f5.com - Produced iApps are no longer have .iapp extension. Now .tmpl is used to follow the conventions. # 2017/06/09 - u.alonsocamaro@f5.com - Implementation of iapp attributes, ie allowing the following to be specified: # # description User defined description. # ignore-verification Set to true to temporarily stop the verification of signature or checksum. Signature or checksum is still retained. # requires-bigip-version-max Specifies the maximum version of BIG-IP required by this template. # requires-bigip-version-min Specifies the minimum version of BIG-IP required by this template. # requires-modules Adds, deletes, or replaces the list of modules that are required to be provisioned or enabled for this template to work. # # Example usage: # # @iapp(description): "Creates a SIP load balancer" # @iapp(ignore-verification): true # @iapp(requires-bigip-version-min): 12.1.1 # @iapp(requires-modules): { ltm asm } # # Note that the syntax of the iApp attributes is the same of TMSH. # # The iapp attribute unsupported-bigip-versions is an extension that is only applied to iWorkflow. The format is defined by iWorkflow: # # @iapp(unsupported-bigip-versions): ["11.6.1", "12.0.1"] # # There is currently no support for signing or creating a checksum iApps. # # 2017/06/15 - u.alonsocamaro@f5.com - Implementation of iworkflow-json-apl and iworkflow-template-import # 2017/06/15 - u.alonsocamaro@f5.com - version and usage options # 2017/06/22 - u.alonsocamaro@f5.com - Support for iWorkflow variables __pool__addr__ and __pool__port__ for automatic statistics. See https://devcentral.f5.com/wiki/iWorkflow.iWorkflowOpsGuide_v22_7.ashx # 2017/06/27 - u.alonsocamaro@f5.com - iworkflow-template-import previously only created "service" iApps. Now any iApp type can be used. It depends in that template has been previously generated # 2017/06/27 - u.alonsocamaro@f5.com - Introduction of pool member's properties without requiring additional variable types. Found some problems in tmsh and reported in C2469542/ID671372 and C2470063 # 2017/06/27 - u.alonsocamaro@f5.com - Fixed heat-create variable's template section curly braces # 2017/06/27 - u.alonsocamaro@f5.com - Added starting/finished messages when the iApp is run # 2017/06/27 - u.alonsocamaro@f5.com - @properties tag previously used for the presentation has been renamed to @apl for two reasons: # + allow easing migration to tmsh2iapp is migrated to iAppsLX # + @properties is a very generic tag and I might use it for other purposes later on # # 2017/06/28 - u.alonsocamaro@f5.com - Introduction of baseline .t2i generation. Usage: # # tmsh2iapp snapshot-create # # tmsh2iapp snapshot-create # # tmsh2iapp t2i-create # # The snapshots are stored in /var/tmp and the resulting .t2i is stored in the same folder too with the name baseline--.t2i # # 2017/07/11 - u.alonsocamaro@f5.com - pool__addr and pool__port are no longer mandated for iworkflow templates # 2017/07/12 - u.alonsocamaro@f5.com - Removal support for per BIG-IP variables # 2017/07/13 - u.alonsocamaro@f5.com - Introduction of importable objects, ie: # # @import(ssl-cert): __cert1__ __cert2__ __cert3__ # @import(ssl-crl): __crl1__ # @import(ssl-csr): __csr1__ __csr2__ # @import(ssl-key): __key1__ __key2__ # @import(data-group): __dg1__ # @import(external-monitor): __mon1__ # # Where ____ are variables in the .t2i file. The @import tag will create an additional automatic variable named ___import__ where the URL from where to import the actual object to be imported. # # This URL can be a local file (ie: file:///tmp/mycert.crt), a remote web server (ie: http://mywebserver.local.net/mycert.crt) etc... any curl URL basically # # The types of objects that can be imported depend on the BIG-IP version, in this release up to v13 object types are supported: # # apache-ssl-cert SSL certificates management # browser-capabilities-db browser capabilities DB file management # dashboard-viewset # data-group External Data Group files management # device-capabilities-db Device capabilities DB file management # external-monitor External Monitor files management # ifile iFile files management # lwtunneltbl LW4o6 table files management # ssl-cert SSL certificates management # ssl-crl SSL CRL files management # ssl-csr SSL Certificate Signing Request # ssl-key SSL certificate keys management # # Note for data-group variables a @property(____) attribute must be set, this is used to: # # - specify the type of the records in the data-group (ie: string|integer|ip). This is mandatory. # - the separator used for the records. This is optional. # # The syntax follows tmsh syntax, see the examples for more info. # # Search for import_file_types and import_perl_types variables for implementation details. Main functions are attribute_import_type, iapp_implementation_import. # # 2017/07/17 - u.alonsocamaro@f5.com - preparation functions for @import(asm-policy) # 2017/07/17 - u.alonsocamaro@f5.com - All foreach loops on variable's arrays have been sorted # 2017/07/18 - u.alonsocamaro@f5.com - Initial support of LTM policies # # Didn't find a way of loading them with tmsh::load hence these will be created with tmsh::create which requires extracting # the relevant section of the .t2i file. The load of the .t2i is now done incrementally. # # 2017/07/20 - u.alonsocamaro@f5.com - Initial support of ASM policies, they don't work yet unless using already imported policies. Researching this issue. # 2017/07/21 - u.alonsocamaro@f5.com - Support comments within the .t2i file by using the # symbol # 2017/07/21 - u.alonsocamaro@f5.com - Made more relaxed the usage of @ and #. They can have spaces from the beginning of the line now. They must match ^\s*(@|#) # 2017/07/31 - u.alonsocamaro@f5.com - Added check_variable_names to avoid mistake of using hyphen in variable names # 2017/08/01 - u.alonsocamaro@f5.com - FIX: @service_folder can now be really placed anywhere # 2017/08/02 - u.alonsocamaro@f5.com - FIX: @service_folder can now be really placed anywhere (part 2) # 2017/08/02 - u.alonsocamaro@f5.com - FIX: incremental loader would previously fail if policy is at the top of the .t2i file # 2017/09/28 - u.alonsocamaro@f5.com - Introduction of per bigip's variables. I call these __local__ variables and are intended to be used for base configs / not sync'ed configs like non-floating self-IPs # # RATIONALE # # The behaviour of iApps (in general) for non-floating/not sync'ed configuration elements is the following: # # - The iApp template is sync'ed # - On instantiation of the iApp in a BIG-IP the parameters (ie: "list sys application service ") will be also sync'ed to other BIG-IPs # but the iApps are not re-run so that local configuration is not generated in the BIG-IPs where the iApp was instantiated in the first place. # - The iApp's parameters can be changed in the other BIG-IPs where the iApp was not instantiated initially and the appropiate configuration for the BIG-IP would be in place. # # This iApp behaviour has the following inconvenients: # # - Even after config-sync When looking in the GUI of the different BIG-IPs the iApps parameters will look inconsistent in the BIG-IPs # with the exception the last one where the iApp was instantiated. # # Note although they look like inconsistent the actual local config is correct because the actual config is generated in each BIG-IP and it is not sync'ed on config-sync. # # - Because the iApp parameters change across BIG-IPs blindly re-configuring an iApp might lead to wrong configuration being applied. # # Local variables avoid these problems. Note that although they still require to be executed in all nodes at deletion time it is # only required to do a delete in any BIG-IP and then sync the configuration. # # USING __local__ VARIABLES # # The syntax is very similar to regular variables but adding the __local prefix. An example follows: # # @label(__local__self_vxlan__): IP address with prefix of the self interface connected to the VxLAN # # net self self-vxlan-__vxlanid__ { # address __local__self_vxlan__ # traffic-group /Common/traffic-group-local-only # vlan vxlan-__vxlanid__ # } # # # When there is a __local__ variable defined in a .t2i file the tmsh2iapp tool automatically generates an additional iApp parameter to indicate the BIG-IPs host names # where the values are going to be applied following the same order. This automatic variable is called local__bigip_names. # # Following the above example if we apply the next values (ie: in the GUI): # # local__bigip_names: bigip1 bigip2 # local__self_vxlan: 1.2.3.4/24 1.2.3.5/24 # # Then bigip1 will apply the value 1.2.3.4/24 and bigip2 will apply the value 1.2.3.5/24. # # Note that the names of the BIG-IP's are matched with the beginning of the BIG-IP's FQDN's. It is not necessary to specify the full name, # it can be "bigip1.subdomain1 bigip1.subdomain2" or just "bigip1 bigip2" as shown in the example. # # The values stored in the variables are defined as TCL lists, ie you can use the following formats when assigning the values for the different BIG-IPs # # Exemple 1: value1 value2 value3 value4 # Example 2: {value1} {value2} {value3} {value4} # Example 3: {some multi value1} {} {another multi value3} {and yet one more multi value4} # # In other words, the two benefits of defining them as TCL lists allow for: # # - multi-word values # - empty values are possible too # # # # 2017/09/28 - u.alonsocamaro@f5.com - FIX: removed sort of vartypes and only sorted actual variable names. # # This at random generated wrong iApps. ie: running the tmsh2iapp over the same .t2i file eventually generated different iApps. # # Note: if your iApp was properly generated you should not worry about existing generated iApps. # # 2017/09/28 - u.alonsocamaro@f5.com - FIX: incremental loader: do not longer remove lines that contain a hash # 2017/10/13 - u.alonsocamaro@f5.com - Implemented "nofolder" and "nofolder-disable-strict-updates" options # 2017/10/13 - u.alonsocamaro@f5.com - Implemented @partition variable # 2018/02/28 - cstubbs@gmail.com - Detect default route domain for deployment partition # 2018/02/28 - cstubbs@gmail.com - Improved auto-creation of nodes by way of pool members within partitions that have non-default (non-zero) route domain # 2018/03/01 - cstubbs@gmail.com - Implemented @routedomainid variable # 2018/05/23 - u.alonsocamaro@f5.com - Renamed @routedomainid as @defaultrd # 2018/05/23 - u.alonsocamaro@f5.com - Added @option attribute for generic options. # # The first option added is "default-route-domain" which can be true (default) or false. This allows disabling the new default behaviour of using partition's # default-route-domain when creating pool members. @defaultrd is in any case defined. # # Example usage: # # @option(default-route-domain): "false" # # 2018/05/23 - u.alonsocamaro@f5.com - Applying @defaultrd to pool members is not performed when (1) @defaultrd is 0 or (new condition) their addresses already contain % # 2018/05/23 - u.alonsocamaro@f5.com - Renamed ansible-create to ansible-playbook # 2018/05/23 - u.alonsocamaro@f5.com - Implemented ansible-role # 2018/05/24 - u.alonsocamaro@f5.com - FIX: $changepath is now only once at the top of the implementation # 2018/05/24 - u.alonsocamaro@f5.com - FIX: LTM policies are now implemented with "add" instead of "replace-all-with" allowing re-configuration without eliminating rules not added by this iApp # 2018/05/24 - u.alonsocamaro@f5.com - Added preliminary ILX workspace import support: @import(ilx-workspace): but not working yet # 2018/05/24 - u.alonsocamaro@f5.com - FIX: moved $changepath before imports # 2018/05/25 - u.alonsocamaro@f5.com - Added auto parameter to allow specifying the iApp type to be generated as @option(iapp-type) in the .t2i file # 2018/05/25 - u.alonsocamaro@f5.com - $changepath is now a function and some other small cleanups # 2018/05/28 - u.alonsocamaro@f5.com - Several small fixes # 2018/05/30 - u.alonsocamaro@f5.com - @import(asm-policy) is now supported with upcoming BIG-IP v14 or using v13 + fixes for ID693694 and ID675232. # Note that to due to pending fix for ID721717 in order to allow iApp reconfiguration the iApp has to be created with -disable-strict-updates # 2018/05/31 - u.alonsocamaro@f5.com - FIX: stray character in heat_parameters_file # 2018/06/04 - u.alonsocamaro@f5.com - Added container-configmap feature to create config maps for the BIG-IP Controller aka Container Connector # 2018/06/04 - u.alonsocamaro@f5.com - Introduction of attribute @json(list) to allow handling of JSON lists by tmsh2iapp by transforming the JSONlist into a space separated list. # # This feature has been created for handling multiple-choice/multiple-select survey variables in Ansible Tower but it might be useful for other use cases too. # # In Ansible Tower these variables are defined in yaml as: # # sample_list: # - value1 # - value2 # # And converted into JSON as "['value1', 'value2']" in a variable which will be a parameter of the iApp. # # This new feature would transform this JSON list into "value1 value2" which is the native tmsh2iapp format # # For usage define one @json attribute statement for each variable. This might change in the future. # # @json(list): __variable1__ # @json(list): __variable2__ # # # CAVEAT: It has been found that values that have quoted values with spaces within the quotes fail. Seems a problem in the REST worker. # # example of this issue: - 'key4 \{ data "value 4" \}' # # NOTE: At the moment PEM URL categories and local variables don't support JSON list formatted values. # # 2018/06/04 - u.alonsocamaro@f5.com - Added iapp_implementation_variables_log to print all variables when the iApp is being instantiated # 2018/07/17 - u.alonsocamaro@f5.com - Added @apl support allowing the use of multichoice, editchoice and choice GUI inputs $tmsh2iapp_version= "20180717.1"; # use strict; binmode STDOUT, ":utf8"; use utf8; use JSON; use File::Basename; use Text::Balanced qw (extract_codeblock); $debug= 0; sub check_iworkflow_variables; sub check_variable_names; sub attribute_option_default_route_domain; sub attribute_option_iapp_type; sub attribute_import_type; $cfgdir="/var/tmp"; if ($#ARGV == -1) { print_synopsis(); exit 1; } if ($#ARGV == 0) { if ($ARGV[0] eq "version") { print_version(); exit 0; } elsif ($ARGV[0] eq "usage") { print_usage(); exit 0; } else { print STDERR "Wrong usage, check the possible options.\n"; print_synopsis(); exit 1; } } if (($#ARGV == 1) && ($ARGV[0] ne "auto") && ($ARGV[0] ne "service") && ($ARGV[0] ne "service-disable-strict-updates") && ($ARGV[0] ne "common") && ($ARGV[0] ne "common-disable-strict-updates") && ($ARGV[0] ne "nofolder") && ($ARGV[0] ne "nofolder-disable-strict-updates") && ($ARGV[0] ne "system") && ($ARGV[0] ne "heat-create") && ($ARGV[0] ne "ansible-playbook") && ($ARGV[0] ne "ansible-role") && ($ARGV[0] ne "iworkflow-json-apl") && ($ARGV[0] ne "iworkflow-template-import") && ($ARGV[0] ne "container-configmap") && ($ARGV[0] ne "snapshot-create")) { print STDERR "Wrong usage, check the possible options shown next\n\n"; print_synopsis(); exit 1; } if (($#ARGV == 1) && ($ARGV[0] eq "snapshot-create")) { check_this_is_bigip(); make_snapshot($ARGV[1]); exit(0); } if (($#ARGV == 2) && ($ARGV[0] eq "t2i-create")) { check_this_is_bigip(); diff_snapshots($ARGV[1], $ARGV[2]); exit(0); } if ($#ARGV >= 2) { print STDERR "Wrong usage, check the possible options shown next\n\n"; print_synopsis(); exit 1; } ### From now on all the processing is for creating an output based on a .t2i file ### $t2i=$ARGV[1]; if (! ($t2i=~ m/\.t2i$/)) { print STDERR "Wrong usage: the template file must have .t2i extension. See further details below.\n\n"; print_synopsis(); exit 1; } ### Variables that must exist $iapp_name= basename($t2i); # iApp name is basename without extension .t2i $iapp_name=~ s/\.t2i$//; # Resulting configuration file after template gets the variables replaced by their values $cfgfile= $iapp_name . ".cfg"; # iApp output file name $iapp_filename= $t2i; $iapp_filename=~ s/\.t2i$/\.tmpl/; $iapp_filename_relative= basename($iapp_filename); # APL json output file name $apl_filename= $t2i; $apl_filename=~ s/\.t2i$/\.apl.json/; # IMPORT json output file name $import_filename= $t2i; $import_filename=~ s/\.t2i$/\.import.json/; ### Default settings $option{"default-route-domain"}= "true"; $iapp{"description"}= "\"iApp $iapp_name generated with tmsh2iapp $tmsh2iapp_version\""; ### open TMPL, "<", $t2i or die "Can't open template file $t2i: $!"; my $raw_content = do { local $/; }; close TMPL; # remove the attributes but not @service_folder / @partition / @defaultrd $content= join("\n", grep(!/^\s*(@(label|apl|properties|iapp|import|json|option))/, split(/\n/, $raw_content))); # get the variables from the t2i file my @matches = uniq ( $content =~ /(__pm__.+?__|__dr__.+?__|__fwal__.+?__|__fwpl__.+?__|__urlcat_match__.+?_.+?__|__urlcat_nomatch__.+?_.+?__|__pool__.+?__|__local__.+?__|__.+?__)/g ); check_variable_names(\@matches); # we separate the items in arrays depending the type of variables. All have to be registered in the @vartypes array. my @variables= grep(!/^(__pm|__dr|__fwal|__fwpl|__app_service__|__urlcat_match__|__urlcat_nomatch__|__pool__|__local__)/, @matches); my @pool_members= grep(/^__pm/, @matches); my @data_records= grep(/^__dr/, @matches); my @fw_address_list= grep(/^__fwal/, @matches); my @fw_port_list= grep(/^__fwpl/, @matches); my @urlcat_match_list= grep(/^__urlcat_match/, @matches); my @urlcat_nomatch_list= grep(/^__urlcat_nomatch/, @matches); my @iworkflow_variables= grep(/^__pool/, @matches); # __pool__addr__ and __pool__port__ are currently a constrain to allow the iApp to expose stats automatically to iWorkflow # check_iworkflow_variables(\@iworkflow_variables); my @localvars= grep(/^__local/, @matches); my @vartypes= (\@variables, \@pool_members, \@data_records, \@fw_address_list, \@fw_port_list, \@urlcat_match_list, \@urlcat_nomatch_list, \@iworkflow_variables, \@localvars); my @vartypes_desc= ("General variables", "Pool members", "Internal data-group records", "Firewall address lists", "Firewall port lists", "PEM URL match category lists", "PEM URL no match category lists", "iWorkflow VIP address and VIP port variables", "Per BIG-IP local variables"); my @import_file_types= ("apache-ssl-cert", "browser-capabilities-db", "dashboard-viewset", "data-group", "device-capabilities-db", "external-monitor", "ifile", "lwtunneltbl", "ssl-cert", "ssl-crl", "ssl-csr", "ssl-key", "asm-policy", "ilx-workspace"); my @import_perl_types= @import_file_types; # check if the template contains attributes, if so gather them... my @attribute_lines= grep /^\s*@/, split(/\n/, $raw_content); foreach $al (@attribute_lines) { $attribute= ""; $variable= ""; $value= ""; print STDERR "parsing: attribute line: $al\n" if ($debug); $_= $al; if (/@(label|apl|properties|iapp|import|json|option)\((.*?)\):\s*(.*)/) { $attribute= $1; $variable= $2; $value= $3; print STDERR "attribute: $attribute, variable: $variable, value: $value\n" if ($debug); } elsif (/\@service_folder/) { # Do nothing, @service_folder can be anywhere next; } elsif (/\@partition/) { # Do nothing, @partition can be anywhere next; } elsif (/\@defaultrd/) { # Do nothing, @defaultrd can be anywhere next; } elsif ($attribute eq "json") { if ($variable ne "list") { print STDERR "Non supported json type \"$variable\". The offending line is shown next: $al\n"; exit(1); } $json_list{$value}= "true"; } else { print STDERR "Aborting due to unexpected attribute line. The offending line is shown next: $al\n"; exit(1); } if ($attribute eq "label") { $labels{$variable}= $value; } elsif ($attribute eq "apl") { $apl{$variable}= $value; } elsif ($attribute eq "properties") { check_legacy_properties_usage($variable, $value); $properties{$variable}= $value; } elsif ($attribute eq "iapp") { $iapp{$variable}= $value; } elsif ($attribute eq "option") { if ($variable eq "default-route-domain") { attribute_option_default_route_domain($value); } elsif ($variable eq "iapp-type") { attribute_option_iapp_type($value); } else { print STDERR "Aborting due to unknown \@option $variable\n"; exit(1); } } elsif ($attribute eq "import") { $arg= $variable; # rename it for clarity: in the case of @import the argument of is not a variable name attribute_import_type($arg, $value); } elsif ($attribute eq "json") { if ($variable ne "list") { print STDERR "Non supported json type \"$variable\". The offending line is shown next: $al\n"; exit(1); } $json_list{$value}= "true"; } else { # Given the check above this should never be triggered print STDERR "Aborting due to unknown attribute \"$attribute\" in the t2i template. The offending line is shown next: $al\n"; exit(1); } } for my $i (0 .. $#import_file_types) { # the perl variables are the same as tmsh but we replace "-" with "_"; $import_perl_types[$i]=~ s/-/_/g; } # Sanity checks if (($ARGV[0] eq "system") && ($raw_content =~ /ltm pool/)) { print STDERR "Warning: pools should not be specified when the system option is used. This will trigger an error when assigning members to it\n"; } if ($ARGV[0] eq "auto") { print STDERR "Aborting: \"auto\" specified in the command line but no valid \@option(iapp-type) found in the .t2i file\n"; exit(1); } ################################################################################################### # Command to execute on the .t2i file if ($ARGV[0] eq "heat-create") { heat_create(); } elsif ($ARGV[0] eq "ansible-playbook") { ansible_playbook(); } elsif ($ARGV[0] eq "ansible-role") { ansible_role(); } elsif ($ARGV[0] eq "iworkflow-json-apl") { iworkflow_json_apl(1); } elsif ($ARGV[0] eq "iworkflow-template-import") { iworkflow_json_import(); } elsif ($ARGV[0] eq "container-configmap") { container_configmap(); } else { iapp_create(1); } exit 0; ################################################################################################### sub iapp_create { my $create_file= $_[0]; if ($create_file) { open IAPP, ">", $iapp_filename or die "Can't open file where the iApp would be written ($iapp_filename): $!"; } $iapp= iapp_head(); $iapp.= iapp_implementation_begin(); $iapp.= iapp_implementation_procs(); $iapp.= iapp_implementation_changepath(); $iapp.= iapp_implementation_import(); $iapp.= iapp_implementation_variables_log(); $iapp.= iapp_implementation_incremental_loader(); $iapp.= iapp_implementation_pool_members_modify(); $iapp.= iapp_implementation_data_records_modify(); $iapp.= iapp_implementation_afm_address_list_modify(); $iapp.= iapp_implementation_afm_port_list_modify(); $iapp.= iapp_implementation_pem_urlcat_modify(); if (($ARGV[0] eq "service-disable-strict-updates") || ($ARGV[0] eq "common-disable-strict-updates") || ($ARGV[0] eq "nofolder-disable-strict-updates")) { $iapp.= iapp_implementation_disable_strict_updates(); } $iapp.= iapp_implementation_end(); $iapp.= iapp_presentation(); $iapp.= iapp_tail(); if ($create_file) { print IAPP $iapp; close IAPP or die "Couldn't close the file for the iApp ($iapp_filename): $!"; print "Written the resulting iApp of type $ARGV[0] to $iapp_filename\n"; } else { return $iapp; } } sub iapp_head { my $head = <<"HEAD"; cli admin-partitions { update-partition Common } sys application template $iapp_name { actions { definition { HEAD return $head; } sub iapp_implementation_begin { my $implementation_begin= << "IMPLEMENTATION_BEGIN"; implementation { puts "################################################################################################################" puts "Starting iApp \$tmsh::app_name.app generated with tmsh2iapp version $tmsh2iapp_version" set partition "/[lindex [split [tmsh::pwd] /] 1]" set partition_name "[lindex [split [tmsh::pwd] /] 1]" if { \$partition == "/" } { puts "Warning: behaviour not well defined when \@partition is \\"/\\"" set defaultrd 0 } else { set obj [tmsh::get_config auth partition \$partition_name default-route-domain] set defaultrd [tmsh::get_field_value [lindex \$obj 0] default-route-domain] } puts "The iApp of type $ARGV[0] is being instantiated in \@partition \$partition, \@defaultrd is \$defaultrd" IMPLEMENTATION_BEGIN return $implementation_begin; } sub iapp_implementation_procs { my $implementation_proc_debug= << "IMPLEMENTATION_PROC_DEBUG"; # Modified from appsvcs_integration_v2.0.tmpl # Print a timestamped debug message to /var/tmp/scriptd.out # Input: headers = TCL list of headers for the log message # msg = The message to log # level = Integer indicated the log level for this message proc debug { headers msg level } { set systemTime [clock seconds] set brackets "" if { [llength \$headers] > 0 } { set brackets [format "\\[%s\\]" [join \$headers "\\]\\["]] } set pre [format "\\[%s %s\\]\\[%s\\]%s" [clock format \$systemTime -format %D] [clock format \$systemTime -format %H:%M:%S] \$::tmsh::app_name.app \$brackets] puts [format "%s %s" \$pre [string map [list "\n" "\n\$pre " ] \$msg]] } IMPLEMENTATION_PROC_DEBUG my $implementation_proc_jsonlist2txt= << "IMPLEMENTATION_PROC_JSONLIST2TXT"; # jsonlist2txt processes JSON list, ie: ['key1 \{ data "value 1" \}', 'key2 \{ data "value 2" \}', 'key3 \{ data "value 3" \}'] # and transforms it into: key1 { data "value 1" } key2 { data "value 2" } key3 { data "value 3" } # # This proc is far from being YAML conformant: the procedure expects that the elements in the list doesn't contain the character "," # proc jsonlist2txt {jlist }{ puts "jsonlist2txt: processing >\$jlist<" # remove the outer brackets set jlist [string trim \$jlist {[]}] # split the elements, using "," as delimiter set jsplit [split \$jlist ,] # trim the quotes and spaces of each element and concatenate them with an space. je stands for json element set retval "" foreach je \$jsplit { set jetrim [subst [string trim \$je {"' }]] append retval \$jetrim " " } # remove leading space set retval [string trim \$retval] puts "jsonlist2txt: output >\$retval<" return \$retval } IMPLEMENTATION_PROC_JSONLIST2TXT my $implementation_proc_curl_save_file= << "IMPLEMENTATION_PROC_CURL_SAVE_FILE"; # Modified from appsvcs_integration_v2.0.tmpl to support local files as well # Run the cURL command and save the URL to a file. Throws a hard error if cURL # exits uncleanly # Input: string url = URL to fetch # string filename = filename to save output to # int error_exit = 1 => throw hard error on non 200 response code # >1 => ignore error and return response code proc curl_save_file { url filename {error_exit 1}} { debug [list curl_save_file start] "url=\$url filename=\$filename error_exit=\$error_exit" 9 set status [catch { exec curl --connect-timeout 5 -k -s -w 'RESPCODE=\%\{response_code\}' -o \$filename \$url } message] debug [list curl_save_file done] "status=\$status message=\$message" 9 if { \$status != 0 } { if { \$error_exit == 1} { error "Error occured while trying to retrieve \$url (status \$status): \$message" } else { return 0 } } return 1 } IMPLEMENTATION_PROC_CURL_SAVE_FILE my $retval= ""; $check_import_type= "asm_policy"; # To avoid the following warning Name "main::asm_policy" used only once: possible typo at ./tmsh2iapp.pl line 477. if (@$check_import_type) { $retval.= $implementation_proc_debug; $retval.= $implementation_proc_curl_save_file; } if (scalar(keys %json_list)){ $retval.= $implementation_proc_jsonlist2txt; } return $retval . "\n"; } sub iapp_implementation_changepath { my $changepath= " "; if ($ARGV[0] eq "system") { $changepath.='tmsh::cd "/"'; } elsif (($ARGV[0] eq "common") || ($ARGV[0] eq "common-disable-strict-updates")) { $changepath.='tmsh::cd "/Common"'; } elsif (($ARGV[0] eq "nofolder") || ($ARGV[0] eq "nofolder-disable-strict-updates")) { $changepath.='tmsh::cd ".."'; } else { $changepath= ""; } $changepath.= "\n\n"; return $changepath; } sub iapp_implementation_import { my $import= ""; my $import_type; my $import_otype; foreach $import_type (@import_perl_types) { foreach $var (sort @$import_type) { $import_otype= $import_type; $import_otype=~ s/_/-/g; $vname= $var; $vname=~ s/__(.*?)__/var__$1/; $url= $var; $url=~ s/__(.*?)__/var__$1_import/; if ($import_otype eq "data-group") { $import.= " set cmd \"tmsh::create sys file $import_otype \${::$vname} source-path \${::$url} $properties{$var}\"\n"; $import.= " puts \$cmd\n"; $import.= " eval \$cmd\n"; } elsif ($import_otype eq "asm-policy") { $import.= " curl_save_file \${::$url} $cfgdir/\${::$vname}.xml\n"; $import.= " set cmd \"tmsh::load asm policy \${::$vname} file $cfgdir/\${::$vname}.xml overwrite\"\n"; $import.= " puts \$cmd\n"; $import.= " eval \$cmd\n"; $import.= " set cmd \"tmsh::modify asm policy \${::$vname} active\"\n"; $import.= " puts \$cmd\n"; $import.= " eval \$cmd\n"; } elsif ($import_otype eq "ilx-workspace") { $import.= " set cmd \"tmsh::create ilx workspace \${::$vname} from-uri \${::$url}\"\n"; $import.= " puts \$cmd\n"; $import.= " eval \$cmd\n"; } else { $import.= " set cmd \"tmsh::create sys file $import_otype \${::$vname} source-path \${::$url}\"\n"; $import.= " puts \$cmd\n"; $import.= " eval \$cmd\n"; } } } $import.= "\n"; return $import; } sub iapp_implementation_variables_log { my $sp= " "; my $retval= "\n"; $retval.= $sp . "puts \"Dumping values passed to iApp variables\"\n"; $retval.= $sp . "puts \">>> regular variables\"\n"; foreach $v (sort @variables) { $vname= $v; $vname=~ s/__(.*?)__/var__$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $v >\${::$vname}<\" } else { puts \"> $v is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> pool member variables\"\n"; foreach $pm (sort @pool_members) { $vname= $pm; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $pm >\${::$vname}<\" } else { puts \"> $pm is undefined\" }\n" . "\n"; $vname.= "_properties"; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $pm properties >\${::$vname}<\" } else { puts \"> $pm properties is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> data record variables\"\n"; foreach $dr (sort @data_records) { $vname= $dr; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $dr >\${::$vname}<\" } else { puts \"> $dr properties is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> firewall address list variables\"\n"; foreach $fwal (sort @fw_address_list) { $vname= $fwal; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $fwal >\${::$vname}<\" } else { puts \"> $fwal is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> firewall port list variables\"\n"; foreach $fwpl (sort @fw_port_list) { $vname= $fwpl; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $fwpl >\${::$vname}<\" } else { puts \"> $fwpl is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> url match category list variables\"\n"; foreach $urlcat (sort @urlcat_match_list) { $vname= $urlcat; $vname=~ s/__(.*)__/$1/; $retval.= "if {[info exists {::$vname}]} { puts \"> $urlcat >\${::$vname}<\" } else { puts \"> $urlcat is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> url nomatch category list variables\"\n"; foreach $urlcat (sort @urlcat_nomatch_list) { $vname= $urlcat; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $urlcat >\${::$vname}<\" } else { puts \"> $urlcat is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \">>> iworkflow variables\"\n"; foreach $iwork (sort @iworkflow_variables) { $vname= $iwork; $vname=~ s/__(.*)__/$1/; $retval.= $sp . "if {[info exists {::$vname}]} { puts \"> $iwork >\${::$vname}<\" } else { puts \"> $iwork is undefined\" }\n" . "\n"; } $retval.= $sp . "puts \"End of dumping values passed to iApp variables\"\n"; return $retval; } sub iapp_implementation_variables_instantiation { my $map= ""; my $retval= ""; # tmsh2iapp defined variables $map.= "\@service_folder \$tmsh::app_name.app "; $map.= "\@partition \$partition "; $map.= "\@defaultrd \$defaultrd "; # Plain iApp variables $retval.= "\n"; foreach $v (sort @variables) { $vname= $v; $vname=~ s/__(.*?)__/var__$1/; # is there a value JSON list transformation for this variable? if (defined($json_list{$v})) { $retval.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } $map.= "$v {\${::$vname}} "; } $retval.= "\n"; # The next variables are list types and are replaced with "" or with a default value and a _modify function will instantiate them later foreach $pm (sort @pool_members) { $map.= "$pm {} "; } foreach $dr (sort @data_records) { $vname= $dr; $vname=~ s/__(dr__.*?)__/$1/; $map.= "$dr {} "; } foreach $fwal (sort @fw_address_list) { # Unfortunatly it doesn't allow an empty value $map.= "$fwal 255.255.255.255/32 "; } foreach $fwpl (sort @fw_port_list) { # Unfortunately it doesn't allow an tmpty value $map.= "$fwpl 65535 "; } foreach $urlcat (sort @urlcat_match_list) { $map.= "$urlcat {} "; } foreach $urlcat (sort @urlcat_nomatch_list) { $map.= "$urlcat {} "; } foreach $iwork (sort @iworkflow_variables) { $vname= $iwork; $vname=~ s/__(.*)__/$1/; $map.= "$iwork {\${::$vname}} "; } # app-service $full_app_service= "\$tmsh::app_name" . ".app" . "/" . "\$tmsh::app_name"; $map.= "__app_service__ $full_app_service "; # Actual variable replacement $retval.= " set cfg [string map \"$map\" \$cfg]\n"; return $retval; } sub iapp_implementation_config_load_section { $_= $_[0]; if (m/^\s*$/) { print STDERR "load_section: No content in section: \n$_[0]\n" if ($debug); return ""; } else { print STDERR "load_section: content found in section: \n$_[0]\n" if ($debug); } my $vars_instantiation= iapp_implementation_variables_instantiation(); $vars_instantiation.= iapp_implementation_local_variables_instantiation(); my $txt = << "CFG"; set cfg { $_[0] } CFG my $load_cmd = <<"LOAD_CMD"; set fileId [open $cfgdir/$cfgfile "w"] puts -nonewline \$fileId \$cfg close \$fileId tmsh::load sys config merge file $cfgdir/$cfgfile LOAD_CMD return $txt . $vars_instantiation . $load_cmd; } sub iapp_implementation_create_ltm_policy { my $retval= ""; my $policy= $_[0]; $policy=~ s/\n//g; $policy=~ s/\s+/ /g; $policy=~ s/ltm policy ((\w+|-|_)+)/ltm policy $1 legacy/; $policy=~ s/actions/actions add/g; $policy=~ s/controls/controls add/g; $policy=~ s/conditions/conditions add/g; $policy=~ s/requires/requires add/g; $policy=~ s/rules/rules add/g; $policy=~ s/{(.*)}/$1/g; ### instantiate variables in the policy, this should be moved to a function eventually my $map= ""; ## tmsh2iapp-own variables # Previously this variable was only valid when using service or service-disable-strict-updates $map.= "\@service_folder \$tmsh::app_name.app "; $map.= "\@partition \$partition "; $map.= "\@defaultrd \$defaultrd "; ## Plain variables foreach $v (sort @variables) { $vname= $v; $vname=~ s/__(.*?)__/var__$1/; $map.= "$v {\${::$vname}} "; } $full_app_service= "\$tmsh::app_name" . ".app" . "/" . "\$tmsh::app_name"; $map.= "__app_service__ $full_app_service "; # build the return value $retval = " set policy [string map \"$map\" {$policy}]\n"; $retval.= " puts \"\$policy\"\n"; $retval.= " tmsh::create \$policy\n"; return $retval; } sub iapp_implementation_incremental_loader { @lines= split /^/, $content; $remaining= $content; my $retval= ""; $i= 0; while($i < scalar(@lines)) { $_= $lines[$i]; if (/^\s*ltm policy/) { @prev= @lines[0..$i-1]; @next= @lines[$i..scalar(@lines)-1]; $prev_flat= join("", @prev); $next_flat= join("", @next); print STDERR ">>> prev_flat:\n", $prev_flat, "\n" if ($debug); print STDERR ">>> next_flat:\n", $next_flat, "\n" if ($debug); ($extracted, $remaining, $prefix)= extract_codeblock($next_flat, '{}', '^\s*ltm\s+policy\s+(\w+|-|_)+\s+'); if (!defined($extracted)) { print STDERR "Aborting: could not match a valid ltm policy starting with the line $lines[$i]"; print STDERR "parsing: next_flat is:\n>>>\n$next_flat\n<<<\n" if ($debug); exit(0); } $policy= $prefix . $extracted; print STDERR ">>> extracted:\n", $extracted, "\n" if ($debug); print STDERR ">>> remaining:\n", $remaining, "\n" if ($debug); ### $retval.= iapp_implementation_config_load_section($prev_flat) . "\n"; $retval.= iapp_implementation_create_ltm_policy($policy) . "\n"; ### $i= 0; @lines= split /^/, $remaining; next; } $i= $i +1; } print STDERR ">>> remaining:\n", $remaining, "\n" if ($debug); $retval.= iapp_implementation_config_load_section($remaining); return $retval; } sub iapp_implementation_pool_members_modify { $retval= "\n\n"; foreach $pm (sort @pool_members) { $pname= $pm; # pool name in the tmsh config $pname=~ s/__pm__(.*?)__/$1/; $vname= $pm; # var name in the iapp script $vname=~ s/__(.*)__/$1/; if (defined($json_list{$pm})) { $retval.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } $vname_properties= $vname . "_properties"; $retval.= " if {([info exists {::$vname}]) && ([string length \${::$vname}] > 0)} {\n"; if (defined($json_list{$pm})) { $retval.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } #it ($option{"nodes-in-common"} eq "true") { # #} if ($option{"default-route-domain"} eq "true") { $retval.= " if {(\$defaultrd != 0) && ([string first \"%\" \${::$vname}] == -1)} {\n"; $retval.= " set cmd \"tmsh::modify ltm pool $pname { members replace-all-with { [string map \": %\${defaultrd}:\" \${::$vname}] } }\"\n"; $retval.= " } else {\n"; $retval.= " set cmd \"tmsh::modify ltm pool $pname { members replace-all-with { \${::$vname} } }\"\n"; $retval.= " }\n"; } else { $retval.= " set cmd \"tmsh::modify ltm pool $pname { members replace-all-with { \${::$vname} } }\"\n"; } $retval.= " puts \"\$cmd\"\n"; $retval.= " eval \$cmd\n"; $retval.= " if {([info exists {::$vname_properties}]) && ([string length \${::$vname_properties}] > 0)} {\n"; $retval.= " set cmd \"tmsh::modify ltm pool $pname { members modify { all { \${::$vname_properties} } } }\"\n"; $retval.= " puts \"\$cmd\"\n"; $retval.= " eval \$cmd\n"; $retval.= " }\n"; $retval.= " }\n"; } return $retval; } sub iapp_implementation_data_records_modify { $retval= "\n\n"; foreach $dr (sort @data_records) { $dname= $dr; # data group name in the tmsh config $dname=~ s/__dr__(.*?)__/$1/; $vname= $dr; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $retval.= " if {[info exists {::$vname}] && [string length \${::$vname}] > 0} {\n"; if (defined($json_list{$dr})) { $retval.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } $retval.= " set cmd \"tmsh::modify ltm data-group internal $dname records { replace-all-with { \${::$vname} } }\"\n"; $retval.= " puts \"\$cmd\"\n"; $retval.= " eval \$cmd\n"; $retval.= " }\n"; } return $retval; } sub iapp_implementation_afm_address_list_modify { $tmsh_cmds= "\n\n"; foreach $fwal (sort @fw_address_list) { $fwal_name= $fwal; # firewall address list name in the tmsh config $fwal_name=~ s/__fwal__(.*?)__/$1/; $vname= $fwal; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $tmsh_cmds.= " if {[info exists {::$vname}] && [string length \${::$vname}] > 0} {\n"; if (defined($json_list{$v})) { $tmsh_cmds.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } $tmsh_cmds.= " set cmd \"tmsh::modify security firewall address-list $fwal_name { addresses replace-all-with { \${::$vname} } }\"\n"; $tmsh_cmds.= " puts \"\$cmd\"\n"; $tmsh_cmds.= " eval \$cmd\n"; $tmsh_cmds.= " }\n"; } return $tmsh_cmds; } sub iapp_implementation_afm_port_list_modify { $tmsh_cmds= "\n\n"; foreach $fwpl (sort @fw_port_list) { $fwpl_name= $fwpl; # firewall port list name in the tmsh config $fwpl_name=~ s/__fwpl__(.*?)__/$1/; $vname= $fwpl; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $tmsh_cmds.= " if {[info exists {::$vname}] && [string length \${::$vname}] > 0} {\n"; if (defined($json_list{$v})) { $tmsh_cmds.= " set {::$vname} [jsonlist2txt \${::$vname}]\n"; } $tmsh_cmds.= " set cmd \"tmsh::modify security firewall port-list $fwpl_name { ports replace-all-with { \${::$vname} } }\"\n"; $tmsh_cmds.= " puts \"\$cmd\"\n"; $tmsh_cmds.= " eval \$cmd\n"; $tmsh_cmds.= " }\n"; } return $tmsh_cmds; } sub iapp_implementation_pem_urlcat_modify { # match URL categories $tmsh_cmds= "\n\n"; foreach $urlcat (sort @urlcat_match_list) { $policy_name= $urlcat; $policy_name=~ s/__urlcat_match__(.*?)_(.*?)__/$1/; $rule_name= $urlcat; $rule_name=~ s/__urlcat_match__(.*?)_(.*?)__/$2/; $vname= $urlcat; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $tmsh_cmds.= " set urlcats [regexp -all -inline {\\S+} \${::$vname}]\n"; $tmsh_cmds.= " set urlcats [split \$urlcats]\n"; $tmsh_cmds.= " set i 0\n"; $tmsh_cmds.= " foreach cat \$urlcats {\n"; # # for some unknown funny reason tmsh::modify requires braces after rules and CLI tmsh doesn't: # root@(afm)(cfg-sync Standalone)(Active)(/Common)(tmos)# modify pem policy test.app/customfiltering rules { modify { filterurls { url-categorization-filters add { url_category0 { url-category Social_Networking operation match } } } } } # Syntax Error: "rules" unexpected argument "{" one of the following must be specified: # add, delete, modify, none, replace-all-with $tmsh_cmds.= " set cmd \"tmsh::modify pem policy $policy_name rules { modify { $rule_name { url-categorization-filters add { url_category\$i { url-category \$cat operation match } } } } }\"\n"; $tmsh_cmds.= " puts \"\$cmd\"\n"; $tmsh_cmds.= " eval \$cmd\n"; $tmsh_cmds.= " incr i\n"; $tmsh_cmds.= " }\n"; } # nomatch URL categories $tmsh_cmds.= "\n\n"; foreach $urlcat (sort @urlcat_nomatch_list) { $policy_name= $urlcat; $policy_name=~ s/__urlcat_nomatch__(.*?)_(.*?)__/$1/; $rule_name= $urlcat; $rule_name=~ s/__urlcat_nomatch__(.*?)_(.*?)__/$2/; $vname= $urlcat; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $tmsh_cmds.= " set urlcats [regexp -all -inline {\\S+} \${::$vname}]\n"; $tmsh_cmds.= " set urlcats [split \$urlcats]\n"; $tmsh_cmds.= " set i 0\n"; $tmsh_cmds.= " foreach cat \$urlcats {\n"; # # for some unknown funny reason tmsh::modify requires braces after rules and CLI tmsh doesn't: $tmsh_cmds.= " set cmd \"tmsh::modify pem policy $policy_name rules { modify { $rule_name { url-categorization-filters add { url_category\$i { url-category \$cat operation nomatch } } } } }\"\n"; $tmsh_cmds.= " puts \"\$cmd\"\n"; $tmsh_cmds.= " eval \$cmd\n"; $tmsh_cmds.= " incr i\n"; $tmsh_cmds.= " }\n"; } return $tmsh_cmds; } sub iapp_implementation_local_variables_instantiation { if ((scalar @localvars) == 0) { return ""; } $tmsh_cmds= "\n\n"; $tmsh_cmds.= " set this_bigip [ tmsh::list sys global-settings hostname ]\n"; $tmsh_cmds.= " regexp -line \"hostname (.*)\" \$this_bigip x this_bigip\n"; $tmsh_cmds.= " puts \"Found this BIG-IP is \$this_bigip\"\n"; $tmsh_cmds.= " set index_bigip -1\n"; $tmsh_cmds.= " set list_bigips [split [regexp -all -inline {\\S+} \$local__bigip_names]]\n"; $tmsh_cmds.= " set i 0\n\n"; $tmsh_cmds.= " foreach bigip \$list_bigips {\n"; $tmsh_cmds.= " if {[regexp -nocase ^\$bigip \$this_bigip]} {\n"; # We do not break the foreach loop to catch possible errors and through a user friendly message $tmsh_cmds.= " puts \"Matched \$bigip with \$this_bigip\ using index \$i\"\n"; $tmsh_cmds.= " if {\$index_bigip != -1} {\n"; $tmsh_cmds.= " set i_name [lindex \$i \$list_bigips]\n"; $tmsh_cmds.= " set index_name [lindex \$index_bigip \$list_bigips]\n"; $tmsh_cmds.= " error \"Several matches found for \$this_bigip with indexes \$index_bigip (\$index_name) and \$i (\$i_name)\"\n"; $tmsh_cmds.= " }\n"; $tmsh_cmds.= " set index_bigip \$i\n"; $tmsh_cmds.= " }\n"; $tmsh_cmds.= " incr i\n"; $tmsh_cmds.= " }\n"; $tmsh_cmds.= " if {\$index_bigip == -1} {\n"; $tmsh_cmds.= " error \"Could not find \$this_bigip in list >\$local__bigip_names<\"\n"; $tmsh_cmds.= " }\n\n"; $tmsh_cmds.= " set n_bigips [llength \$list_bigips]\n"; foreach $lv (@localvars) { $vname= $lv; # var name in the iapp script $vname=~ s/__(.*)__/$1/; $tmsh_cmds.= " set ll [llength \${::$vname}]\n"; $tmsh_cmds.= " if {\$ll != \$n_bigips} {\n"; $tmsh_cmds.= " error \"The number of values in the variable $vname (\$ll) is different than the number of BIG-IPs (\$n_bigips), variable value is >\${::$vname}<\"\n"; $tmsh_cmds.= " }\n"; $tmsh_cmds.= "\n"; $tmsh_cmds.= " set value [lindex \${::$vname} \$index_bigip]\n"; $tmsh_cmds.= " puts \"Applying local value >\$value< for $lv\"\n"; $tmsh_cmds.= " set cfg [string map \"$lv {\$value}\" \$cfg]\n"; } return $tmsh_cmds; } sub iapp_implementation_disable_strict_updates { $tmsh_cmds= "\n\n"; if ($ARGV[0] eq "common-disable-strict-updates") { $service_path= "\$tmsh::app_name" . ".app" . "/" . "\$tmsh::app_name"; } elsif ($ARGV[0] eq "nofolder-disable-strict-updates") { $service_path= "\$tmsh::app_name" . ".app" . "/" . "\$tmsh::app_name"; } elsif ($ARGV[0] eq "service-disable-strict-updates") { $service_path= "\$tmsh::app_name"; } $tmsh_cmds.= "set cmd \"tmsh::modify sys application service $service_path strict-updates disabled\"\n"; $tmsh_cmds.= " puts \"\$cmd\"\n"; $tmsh_cmds.= " eval \$cmd\n"; return $tmsh_cmds; } sub iapp_implementation_end { my $implementation_end = << "IMPLEMENTATION_END"; puts "Finished iApp \$tmsh::app_name.app generated with tmsh2iapp version $tmsh2iapp_version" } IMPLEMENTATION_END return $implementation_end; } sub iapp_presentation { my $txt= << "TXT"; presentation { include "/Common/f5.apl_common" TXT ### variable definitions foreach $vartype (@vartypes) { @vt= @$vartype; if (($#vt +1) > 0) { $_= $vt[0]; # vt_name = variable type name if (/__(.+)__(.+)__/) { $vt_name= $1; # non plain variables type } else { $vt_name= "var"; # plain variables type } $txt= $txt . " section $vt_name {\n"; if ($vt_name eq "local") { # Special processing for local variables $txt= $txt . " string bigip_names display \"xxlarge\"\n"; } foreach $v (sort @$vartype) { $vname= $v; $vname=~ s/__(.*__|)(.+)__/$2/; $input_type= "string"; # By default all variables are strings if (defined($apl{$v})) { # but we can also allow the user to specify multichoice, editchoice and choice in the GUI with the @apl attribute $p= $apl{$v}; $_= $p; if (/^(\s*(multichoice|editchoice|choice)\s+)(.*)/) { $input_type= $2; $p= $3; $p=~ s/\\n/\n/g; # We want to interpret newlines } print STDERR "\@apl attribute for variable \"$vname\" of type \"$input_type\" is \"$p\"\n" if ($debug); } else { $p= "display \"xxlarge\""; } $txt= $txt . " $input_type $vname $p\n"; if ($vt_name eq "pm") { # Special processing for pm properties automatic variables $vname_properties= $vname . "_properties"; $txt= $txt . " string $vname_properties $p\n"; } if (defined($imports{$v})) { # Special processing for imported objects $vname_import= $vname . "_import"; $txt= $txt . " string $vname_import display \"xxlarge\"\n"; } } $txt= $txt . " }\n\n"; } } ### Variables presentation $txt= $txt . " text {\n"; my $i= 0; my $vartype_plain= 1; foreach $vartype (@vartypes) { @vt= @$vartype; if (($#vt +1) > 0) { $_= $vt[0]; # vt_name = variable type name if (/__(.+)__(.+)__/) { $vt_name= $1; # non plain variables type } else { $vt_name= "var"; # plain variables type } $txt= $txt . "\n $vt_name \"$vartypes_desc[$i]\"\n"; if ($vt_name eq "local") { # Special processing for local variables $txt= $txt . " $vt_name.bigip_names \"List of all the BIG-IP names in the cluster\"\n"; } foreach $v (sort @$vartype) { if (defined($labels{$v})) { $label= "$labels{$v}"; } else { # we append a __var prefix if it is plain var type if ($vartype_plain == 1) { $prefix= "__var"; } else { $prefix= ""; } $label= $prefix . $v; } $vname= $v; $vname=~ s/__(.*__|)(.+)__/$2/; $txt= $txt . " $vt_name.$vname \"$label\"\n"; if ($vt_name eq "pm") { # Special processing for pm properties automatic variables $vname_properties= $vname . "_properties"; $label= "Properties for " . $label; $txt= $txt . " $vt_name.$vname_properties \"$label\"\n"; } if (defined($imports{$v})) { # Special processing for imported objects $vname_import= $vname . "_import"; $label= "URL for " . $label; $txt= $txt . " $vt_name.$vname_import \"$label\"\n"; } } } $i= $i +1; $vartype_plain= 0; } ### my $presentation_end = << "PRESENTATION_END"; } } PRESENTATION_END $txt= $txt . $presentation_end; return $txt; } sub iapp_tail { my $tail = <<"TAIL"; role-acl { admin manager resource-admin } } } TAIL foreach my $attr (sort keys %iapp) { if ($attr ne "unsupported-bigip-versions") { $tail.= " $attr $iapp{$attr}\n"; } } $tail.= "\n}\n"; return $tail; } ### The next functions are for the heat_create option sub heat_create { # HEAT output file names $heat_filename= $t2i; $heat_filename=~ s/\.t2i$/\.yaml/; $heat_params_filename= $t2i; $heat_params_filename=~ s/\.t2i$/\.parameters/; open HEAT, ">", $heat_filename or die "Can't open file where the HEAT template would be written ($heat_filename): $!"; print HEAT heat_head(); print HEAT heat_parameters(); print HEAT heat_resources_static(); print HEAT heat_resources_variables(); close HEAT or die "Couldn't close the file for the HEAT template ($heat_filename): $!"; print "Written the resulting HEAT template to $heat_filename\n"; open HEAT_PARAMS, ">", $heat_params_filename or die "Can't open file where the HEAT parameters would be written ($heat_params_filename): $!"; print HEAT_PARAMS heat_parameters_file(); close HEAT_PARAMS or die "Couldn't close the file for the HEAT parameters ($heat_params_filename): $!"; print "Written a sample HEAT parameters file for the HEAT template in $heat_params_filename\n"; } sub heat_head { my $head = << "HEAD"; heat_template_version: 2015-04-30 description: HEAT deployer of iApp template $iapp_name HEAD return $head; } sub heat_parameters { my $parameters = << "DEFAULT_PARAMS"; parameters: iapp_service_name: type: string label: iApp Service Name default: $iapp_name ve_instance: type: string label: BIG-IP's IP address description: Where the iApp will be deployed. Typically this is the IP address of the management interface bigip_un: type: string label: BigIP Login Username default: admin bigip_pw: type: string label: BigIP Login Password default: admin DEFAULT_PARAMS foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $parameters= $parameters . " " . local__bigip_names . ":\n"; $parameters= $parameters . " " . " " . "type: string\n"; $parameters= $parameters . " " . " " . "label: List of all BIG-IP names in the cluster\n"; } } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $parameters= $parameters . " " . $v_name . ":\n"; $parameters= $parameters . " " . " " . "type: string\n"; if (defined($labels{$v})) { $parameters= $parameters . " " . " " . "label: " . $labels{$v} . "\n"; } $_= $v_name; # Special handling for pool members properties if (/^pm__/) { $v_name_properties= $v_name . "_properties"; $parameters= $parameters . " " . $v_name_properties . ":\n"; $parameters= $parameters . " " . " " . "type: string\n"; if (defined($labels{$v})) { $parameters= $parameters . " " . " " . "label: " . "Properties for " . $labels{$v} . "\n"; } } if (defined($imports{$v})) { # Special processing for imported objects $v_name_import= $v_name . "_import"; $parameters= $parameters . " " . $v_name_import . ":\n"; $parameters= $parameters . " " . " " . "type: string\n"; if (defined($labels{$v})) { $parameters= $parameters . " " . " " . "label: " . "URL for " . $labels{$v} . "\n"; } } } } return $parameters; } sub heat_resources_static { my $resources = << "RESOURCES"; resources: bigip_rsrc: type: F5::BigIP::Device properties: ip: { get_param: ve_instance } username: { get_param: bigip_un } password: { get_param: bigip_pw } partition: type: F5::Sys::Partition depends_on: bigip_rsrc properties: name: Common bigip_server: { get_resource: bigip_rsrc } iapp_template: type: F5::Sys::iAppFullTemplate depends_on: [bigip_rsrc, partition] properties: bigip_server: { get_resource: bigip_rsrc } partition: { get_resource: partition } full_template: { get_file: $iapp_filename_relative } iapp_service: type: F5::Sys::iAppService depends_on: iapp_template properties: bigip_server: { get_resource: bigip_rsrc } partition: { get_resource: partition } traffic_group: none name: { get_param: iapp_service_name } template_name: $iapp_name RESOURCES return $resources; } sub heat_resources_variables { $vars_count= 0; foreach $vartype (@vartypes) { @vt= @$vartype; $vars_count+= ($#vt +1); } if ($vars_count == 0) { return ""; } my $section= << "VARIABLES"; variables: str_replace: params: VARIABLES foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $section.= " " . "_local__bigip_names_" . ":" . " { get_param: local__bigip_names }\n"; } } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $section.= " " . "_" . $v_name . "_" . ":" . " { get_param: " . $v_name . " }\n"; $_= $v_name; # Special handling for pool members properties if (/^pm__/) { $v_name_properties= $v_name . "_properties"; $section.= " " . "_" . $v_name_properties . "_" . ":" . " { get_param: " . $v_name_properties . " }\n"; } if (defined($imports{$v})) { # Special processing for imported objects $v_name_import= $v_name . "_import"; $section.= " " . "_" . $v_name_import . "_" . ":" . " { get_param: " . $v_name_import . " }\n"; } } } $section.= " template: |\n"; $section.= " [{\n"; $vars_i= 1; $vartype_plain= 1; foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $section.= " \"name\": " . "\"local__bigip_names\",\n"; $section.= " \"encrypted\": \"no\",\n"; $section.= " \"value\": " . "\"_local__bigip_names_\"\n"; $section.= " }, {\n"; } } # we append a var__ prefix if it is plain var type if ($vartype_plain == 1) { $prefix= "var__"; $vartype_plain= 0; } else { $prefix= ""; } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $section.= " \"name\": " . "\"" . $prefix . $v_name . "\",\n"; $section.= " \"encrypted\": \"no\",\n"; $section.= " \"value\": " . "\"_" . $v_name . "_\"\n"; $_= $v_name; if (/^pm__/) { # Special processing for pool members $section.= " }, {\n"; $v_name_properties= $v_name . "_properties"; $section.= " \"name\": " . "\"" . $prefix . $v_name_properties . "\",\n"; $section.= " \"encrypted\": \"no\",\n"; $section.= " \"value\": " . "\"_" . $v_name_properties . "_\"\n"; } if (defined($imports{$v})) { # Special processing for imported objects $section.= " }, {\n"; $v_name_import= $v_name . "_import"; $section.= " \"name\": " . "\"" . $prefix . $v_name_import . "\",\n"; $section.= " \"encrypted\": \"no\",\n"; $section.= " \"value\": " . "\"_" . $v_name_import . "_\"\n"; } if ($vars_i < $vars_count) { $section.= " }, {\n"; } $vars_i= $vars_i +1; } } $section.= " }]\n"; return $section; } sub heat_parameters_file { my $default_params= << "DEFAULT_PARAMS"; parameters: # iapp_service_name: $iapp_name ve_instance: 1.2.3.4 # bigip_un: admin # bigip_pw: admin DEFAULT_PARAMS my $params= $default_params; foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $params= $params . " " . local__bigip_names . ":\n"; } } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $params= $params . " " . $v_name . ":\n"; $_= $v_name; if (/^pm__/) { # Special processing for pool members $v_name_properties= $v_name . "_properties"; $params= $params . " " . $v_name_properties . ":\n"; } if (defined($imports{$v})) { # Special processing for imported objects $v_name_import= $v_name . "_import"; $params= $params . " " . $v_name_import . ":\n"; } } } return $params; } ### The next functions are for the ansible-playbook and ansible-role options sub ansible_playbook { # Ansible output file $ansible_filename= $t2i; $ansible_filename=~ s/\.t2i$/\.yaml/; open ANSIBLE, ">", $ansible_filename or die "Can't open file where the Ansible playbook would be written ($ansible_filename): $!"; print ANSIBLE ansible_playbook_head(); print ANSIBLE ansible_variables(); close ANSIBLE or die "Couldn't close the file for the Ansible playbook ($ansible_filename): $!"; print "Written Ansible playbook $ansible_filename\n"; } sub ansible_playbook_head { my $head = << "HEAD"; --- - hosts: tasks: - name: Add to the BIG-IP library the iApp file $iapp_name.tmpl delegate_to: localhost bigip_iapp_template: content: "{{ lookup('template', '$iapp_name.tmpl') }}" # force: server: state: "present" - name: Deploy the iApp $iapp_name delegate_to: localhost bigip_iapp_service: name: template: $iapp_name # force: server: state: "present" HEAD return $head; } sub ansible_role_print { my $role= << "ROLE1"; --- ## Deploy section - name: Add to the BIG-IP library the iApp file $iapp_name.tmpl delegate_to: localhost bigip_iapp_template: content: "{{ lookup('template', '$iapp_name.tmpl') }}" # force: server: "{{ bigip }}" state: "present" tags: - add - name: Deploy the iApp $iapp_name delegate_to: localhost bigip_iapp_service: name: "{{ iapp_service_name }}" template: $iapp_name force: true server: "{{ bigip }}" state: "present" ROLE1 $role.= ansible_variables(); $role.= << "ROLE2"; tags: - add ROLE2 $role.= << "ROLE3"; ### Undeploy section - name: Undeploy the iApp $iapp_name delegate_to: localhost bigip_iapp_service: name: "{{ iapp_service_name }}" template: $iapp_name force: true server: "{{ bigip }}" state: "absent" tags: - del - name: Delete from BIG-IP the iApp file $iapp_name.tmpl delegate_to: localhost bigip_iapp_template: content: "{{ lookup('template', '$iapp_name.tmpl') }}" force: true server: "{{ bigip }}" state: "absent" register: result failed_when: - not result|success - "'referenced by one or more applications' not in result.msg" tags: - del ROLE3 return $role; } sub ansible_variables_value{ my $variable= $_[0]; if ($ARGV[0] eq "ansible-role") { return "\"{{ $variable }}\""; } return "\"\""; } sub ansible_variables{ if ($ARGV[0] eq "ansible-playbook") { # Additional space identation $sp= " "; } else { $sp= ""; } my $vars_count= 0; my $value; foreach $vartype (@vartypes) { @v= $vartype; $vars_count+= $#v +1; } if ($vars_count == 0) { return ""; } my $section= << "VARIABLES"; $sp parameters: $sp variables: VARIABLES $vartype_plain= 1; foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $section.= "$sp - name: \"local__bigip_names\"\n"; $value= ansible_variables_value("local__bigip_names"); $section.= "$sp value: $value\n"; } } # we append a var__ prefix if it is plain var type if ($vartype_plain == 1) { $prefix= "var__"; $vartype_plain= 0; } else { $prefix= ""; } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $v_name= $prefix . $v_name; $section.= "$sp - name: \"$v_name\"\n"; $value= ansible_variables_value($v_name); $section.= "$sp value: $value\n"; $_= $v_name; if (/^pm__/) { # Special handling for pool members $v_name_properties= $v_name . "_properties"; $section.= "$sp - name: \"$v_name_properties\"\n"; $value= ansible_variables_value($v_name_properties); $section.= "$sp value: $value\n"; } if (defined($imports{$v})) { # Special processing for imported objects $v_name_import= $v_name . "_import"; $section.= "$sp - name: \"$v_name_import\"\n"; $value= ansible_variables_value($v_name_import); $section.= "$sp value: $value\n"; } } } return $section; } sub ansible_role { $ansible_directory= "roles" . "/" . $iapp_name . "/" . "tasks"; $ansible_filename= $ansible_directory . "/" . "main.yaml"; system("mkdir -p $ansible_directory"); if ( $? == -1 ) { die "Error while trying to create directory for role $ansible_directory: $!"; } open ANSIBLE, ">", $ansible_filename or die "Can't open file for the Ansible role would be written ($ansible_filename): $!"; print ANSIBLE ansible_role_print(); close ANSIBLE or die "Couldn't close the file for the Ansible role ($ansible_filename): $!"; print "Written Ansible role $ansible_filename\n\n"; print "Don't forget to copy the iApp template $iapp_name.tmpl into the $iapp_name directory\n\n"; } ### The next functions are for the iworkflow-json-apl option sub variable_is_required { my $vname= $_[0]; if (defined($properties{$vname})) { $_= $properties{$vname}; if (/(^|\s+)required($|\s+)/) { return "true"; } } return "false"; } sub iworkflow_json_apl { my $create_file= $_[0]; if ($create_file) { open APL, ">", $apl_filename or die "Can't open file where the APL json would be written ($apl_filename): $!"; } my $json= { }; my $i= 0; # iterates on the JSON elements my $j= 0; # iterates on the variable type descriptions foreach $vartype (@vartypes) { @vt= @$vartype; if (($#vt +1) > 0) { $_= $vt[0]; # vt_name = variable type name if (/__(.+)__(.+)__/) { $vt_name= $1; # non plain variables type } else { $vt_name= "var"; # plain variables type } $json->{'sections'}->[$i]->{'description'}= $vartypes_desc[$j]; $json->{'sections'}->[$i]->{'displayName'}= $vt_name; $i= $i +1; } $j= $j +1; } my $vartype_plain= 1; $i= 0; foreach $vartype (@vartypes) { @vt= @$vartype; if (($#vt +1) > 0) { $_= $vt[0]; # vt_name = variable type name if (/__(.+)__(.+)__/) { $vt_name= $1; # non plain variables type } else { $vt_name= "var"; # plain variables type } $_= $vt_name; # Special handling for local variables if (/^local/) { $json->{'vars'}->[$i]->{name}= "bigip_names"; $json->{'vars'}->[$i]->{isRequired}= "false"; $json->{'vars'}->[$i]->{description}= "List of all BIG-IP names in the cluster"; $json->{'vars'}->[$i]->{displayName}= "List of all BIG-IP names in the cluster"; $json->{'vars'}->[$i]->{section}= "local"; $i= $i +1; } # This sort actually doesn't help because the order of the elements in the json is guaranteed foreach $v (sort @$vartype) { $vname= $v; if (defined($labels{$vname})) { $label= "$labels{$vname}"; } else { # we append a __var prefix if it is plain var type if ($vartype_plain == 1) { $prefix= "__var"; } else { $prefix= ""; } $label= $prefix . $vname; } $short_vname= $vname; $short_vname=~ s/__(.*__|)(.+)__/$2/; $json->{'vars'}->[$i]->{name}= $short_vname; $json->{'vars'}->[$i]->{isRequired}= variable_is_required($vname); $json->{'vars'}->[$i]->{description}= "$label"; $json->{'vars'}->[$i]->{displayName}= "$label"; $json->{'vars'}->[$i]->{section}= $vt_name; $i= $i +1; if ($vt_name eq "pm") { $short_vname_properties= $short_vname . "_properties"; $json->{'vars'}->[$i]->{name}= $short_vname_properties; $json->{'vars'}->[$i]->{isRequired}= variable_is_required($vname); $json->{'vars'}->[$i]->{description}= "Properties for " . "$label"; $json->{'vars'}->[$i]->{displayName}= "Properties for " . "$label"; $json->{'vars'}->[$i]->{section}= $vt_name; $i= $i +1; } if (defined($imports{$v})) { # Special processing for imported objects $short_vname_import= $short_vname . "_import"; $json->{'vars'}->[$i]->{name}= $short_vname_import; $json->{'vars'}->[$i]->{isRequired}= variable_is_required($vname); $json->{'vars'}->[$i]->{description}= "URL for " . "$label"; $json->{'vars'}->[$i]->{displayName}= "URL for " . "$label"; $json->{'vars'}->[$i]->{section}= $vt_name; $i= $i +1; } } } $vartype_plain= 0; } if ($create_file) { my $json_text = to_json($json, {utf8 => 1, pretty => 1}); print APL $json_text; close APL or die "Couldn't close the file for the APL json ($apl_filename): $!"; print "Written the resulting APL json to $apl_filename\n"; } else { return $json; } } sub iworkflow_json_import { open IMPORT, ">", $import_filename or die "Can't open file where the IMPORT json would be written ($import_filename): $!"; my $json= { }; $json->{'name'}= $iapp_name; open IAPP, "<", $iapp_filename or die "Can't open file of the iApp ($iapp_filename). This file has to be generated previously: $!"; $json->{'templateContent'}= do { local $/; }; close IAPP; if (defined($iapp{"requires-bigip-version-min"})) { $json->{'minSupportedBIGIPVersion'}= $iapp{"requires-bigip-version-min"}; } if (defined($iapp{"requires-bigip-version-max"})) { $json->{'maxSupportedBIGIPVersion'}= $iapp{"requires-bigip-version-max"}; } if (defined($iapp{"unsupported-bigip-versions"})) { $json->{'unsupportedBIGIPVersions'}= from_json($iapp{"unsupported-bigip-versions"}); } $json->{'template'}= iworkflow_json_apl(0); my $json_text = to_json($json, {utf8 => 1, pretty => 1}); print IMPORT $json_text; close IMPORT or die "Couldn't close the file for the IMPORT json ($import_filename): $!"; print "Written the resulting IMPORT json to $import_filename\n"; } sub check_variable_names() { my @variables = @{$_[0]}; if (grep(/-/, @variables)) { print STDERR "Aborting: found variables that have hyphen in their name and iApps don't support these. Please rename the following variables: "; foreach $v (@variables) { $_= $v; if (/-/) { print STDERR "$v "; } } print STDERR "\n"; exit(1); } } sub check_iworkflow_variables() { my @v = @{$_[0]}; if (($#v == 1) || ($#v == -1)) { # or we have the two expected variables or we have none return 0; } print STDERR "Aborting: when using iWorkflow's variables both __pool__port__ and __pool__addr__ must be defined. Showing the variables found: "; foreach my $i (@v) { print STDERR "$i "; } print STDERR "\n"; exit(1); } #################################################################################### # Functions for BIG-IP Controller (aka Container Connector) ConfigMaps sub container_configmap { # Ansible output file $configmap_filename= $t2i; $configmap_filename=~ s/\.t2i$/\.yaml/; open CONFIGMAP, ">", $configmap_filename or die "Can't open file where the Container ConfigMap would be written ($configmap_filename): $!"; print CONFIGMAP container_configmap_head(); print CONFIGMAP container_configmap_variables(); print CONFIGMAP container_configmap_tail(); close CONFIGMAP or die "Couldn't close the file for the Container ConfigMap (configmap_filename): $!"; print "Written Container ConfigMap $configmap_filename\n"; } sub container_configmap_head { my $head = << "HEAD"; --- kind: ConfigMap apiVersion: v1 metadata: name: "%%servicename%%" namespace: "%%namespace%%" labels: f5type: virtual-server data: # schema v.0.1.4 is required as of k8s-bigip-ctlr v1.3.0 schema: "f5schemadb://bigip-virtual-server_v0.1.4.json" data: | { "virtualServer": { "backend": { "serviceName": "%%backend-servicename%%", "servicePort": "%%backend-serviceport%%" }, "frontend": { "partition": "%%partition%%", "iapp": "/Common/$iapp_name", "iappPoolMemberTable": { "name": "pool__members", "columns": [ {"name": "addr", "kind": "IPAddress"}, {"name": "port", "kind": "Port"}, {"name": "connection_limit", "value": "0"} ] }, "iappOptions": { "description": $iapp{"description"}, "trafficGroup": "/Common/traffic-group-1" }, HEAD return $head; } sub container_configmap_variables_value{ my $variable= $_[0]; return "\"%%" . $variable . "%%\""; } sub container_configmap_variables { my $after_variable= 0; my $section= << "VARIABLES"; "iappVariables": { VARIABLES $vartype_plain= 1; foreach $vartype (@vartypes) { @vt= @$vartype; # Special handling for local variables if (($#vt +1) > 0) { $_= $vt[0]; if (/^__local__/) { $section.= ",\n" if ($after_variable); $section.= ' "local__bigip_names": '; $section.= container_configmap_variables_value("local__bigip_names"); $after_variable= 1; } } # we append a var__ prefix if it is plain var type if ($vartype_plain == 1) { $prefix= "var__"; $vartype_plain= 0; } else { $prefix= ""; } foreach $v (sort @$vartype) { $v_name= $v; $v_name=~ s/__(.*)__/$1/; $v_name= $prefix . $v_name; $section.= ",\n" if ($after_variable); $section.= " \"$v_name\": "; $section.= container_configmap_variables_value($v_name); $after_variable= 1; $_= $v_name; if (/^pm__/) { # Special handling for pool members $v_name_properties= $v_name . "_properties"; $section.= ",\n" if ($after_variable); $section.= " \"$v_name_properties\": "; $section.= container_configmap_variables_value($v_name_properties); } if (defined($imports{$v})) { # Special processing for imported objects $v_name_import= $v_name . "_import"; $section.= ",\n" if ($after_variable); $section.= " \"$v_name_import\": "; $section.= container_configmap_variables_value($v_name_import); } } } $section.= "\n }\n"; return $section; } sub container_configmap_tail { my $tail = << "TAIL"; } } } TAIL return $tail; } #################################################################################### # Functions for t2i generation sub check_this_is_bigip { if (( -r "/config/bigip.conf") && ( -r "/config/bigip_base.conf" )) { return 1; } return 0; } sub make_snapshot { my $snapshot= $_[0]; system("mkdir -p $cfgdir/tmsh2iapp.$snapshot && cp /config/bigip.conf /config/bigip_base.conf $cfgdir/tmsh2iapp.$snapshot"); if ( $? == -1 ) { die "Error while trying to make snapshot $snapshot: $!"; } print "Created snapshot of configuration files in folder $cfgdir/tmsh2iapp.$snapshot\n"; } sub diff_snapshots { my $snapshot1= $_[0]; my $snapshot2= $_[1]; open(DIFF, "diff -ur $cfgdir/tmsh2iapp.$snapshot1 $cfgdir/tmsh2iapp.$snapshot2 | egrep '^\\+\\+\\+' | wc -l |") || die "Could not geneterate the baseline .t2i (when running first diff command): $!\n"; $changed= do { local $/; }; close(DIFF); if ($changed == 0) { print STDERR "There are no differences between snapshot $snapshot1 and $snapshot2\n"; exit(1); } elsif ($changed == 2) { print STDERR "Warning: Both bigip.conf and bigip_base.conf differ in snapshots $snapshot1 and $snapshot2. An iApp should contain either local of cluster-wide objects but not both\n"; } # We also remove some basic stuff like the app-service field open(DIFF, "diff -ur $cfgdir/tmsh2iapp.$snapshot1 $cfgdir/tmsh2iapp.$snapshot2 | egrep '^\\+' | grep -v app-service | egrep -v '^\\+\\+\\+' | cut -c 2- |") || die "Could not geneterate the baseline .t2i (when running the second diff command): $!"; $baseline= do { local $/; }; $baseline=~ s/\/Common\///g; close DIFF; $output= $cfgdir . "/" . "tmsh2iapp-" . $snapshot1 . "-" . $snapshot2 . ".t2i"; open(OUTPUT, ">", $output) || die "Could not geneterate the baseline file $output: $!"; do { local $/; print OUTPUT $baseline; }; close OUTPUT; print "Written the resulting baseline file to $output\n"; } #################################################################################### # @option(default-route-domain) processing sub attribute_option_default_route_domain { my $value= $_[0]; if (($value ne "true") && ($value ne "false")) { print STDERR "Aborting due to \@option(default-route-domain) is not either \"true\" or \"false\". Value found is $value\n"; exit(1); } $option{"default-route-domain"}= $value; } #################################################################################### # @option(iapp-type) processing sub attribute_option_iapp_type { my $value= $_[0]; if (($ARGV[0] eq "heat-create") || ($ARGV[0] eq "ansible-playbook") || ($ARGV[0] eq "ansible-role") || ($ARGV[0] eq "iworkflow-json-apl") || ($ARGV[0] eq "iworkflow-template-import") || ($ARGV[0] eq "container-configmap") || ($ARGV[0] eq "snapshot-create") || ($ARGV[0] eq "t2i-create")) { return; } if (defined($option{"iapp-type"})) { print STDERR "Aborting due to \@option(iapp-type) has been specified more than once\n"; exit(1); } if ($ARGV[0] ne "auto") { print STDERR "Notice: not using \@option(iapp-type): $value because iapp-type is being overridden in the command line\n"; return; } $ARGV[0]= $value; # Checking of $value is done in the regular $ARGV[0] checking $option{"iapp-type"}= $value; } #################################################################################### # BIG-IP object import functions # Instantiates perl arrays for import types with the the iApp variables in their contents, one array per type sub attribute_import_type { print STDERR "attribute_import_type($_[0], $_[1])\n" if ($debug); my $import_otype= $_[0]; # original type name my $import_type= $import_otype; $import_type=~ s/-/_/g; # the perl variables are the same as tmsh but we replace "-" with "_"; my $value= $_[1]; if (!grep( /^$import_otype$/, @import_file_types)) { print STDERR "Aborting due to unknown \@import of \"$import_otype\" in the t2i template. Check the name is correct.\n"; exit(1); } if (@$import_type) { print STDERR "Aborting due to \"$import_otype\" already defined as \@import. Pleasse correct the .t2i file.\n"; exit(1); } @$import_type= split /\s+/, $value; # we assign the values into an array of each type, ie: ssl_cert[0]= __var_name_1__ ... foreach $var (@$import_type) { $imports{$var}= "true"; # This hash defines all variables that have an import regardless of the type if (!grep(/^$var$/, @variables)) { print STDERR "Error while trying to import $import_otype: there is no variable named $var in the .t2i file. Check that the name is correct.\n"; exit(1); } if ($import_type eq "data_group") { # We do additional checking for data-groups if (!defined($properties{$var})) { print STDERR "Aborting due to \@properties for import data-group variable $var not being defined before the \@import of the variable. Put the \@properties at the top of the .t2i file.\n"; exit(1); } $_= $properties{$var}; if (!/type\s+(string|integer|ip)/) { print STDERR "Aborting due to \@properties for import data-group variable $var doesn't have the type indicated. Possible options are:\n"; print STDERR "type string\n"; print STDERR "type integer\n"; print STDERR "type ip\n\n"; print STDERR "Check the tmsh2iapp documentation for more information.\n"; exit(1); } } } } #################################################################################### # Generic functions sub uniq { my %seen; return grep { !$seen{$_}++ } @_; } sub print_version { print "$tmsh2iapp_version\n"; } sub check_legacy_properties_usage { $v_name= $_[0]; $v_value= $_[1]; $_= $v_value; if (/default|display|required|validator/) { print STDERR "Aborting: \@properties attribute for variable \"$v_name\" is using legacy APL values: \"$v_value\"\n"; print STDERR "You have two options:\n"; print STDERR "- Use a version of tmsh2iapp earlier than 20170627.1\n"; print STDERR "- Rename \@properties to \@apl in the .t2i file (recommended option)\n"; exit(1); } } sub print_synopsis { my $synopsis= << "SYNOPSIS"; Synopsis: - iApp creation: tmsh2iapp.pl auto