Icon

TemplateFx | Dynamic Templating Tool

by Chris Mason <chris@netnix.org>

Latest Version: 2.54 (Build 16243)
Released: 30th August 2016

1. Introduction

TemplateFx is a Dynamic Templating Tool which allows you to generate outputs based on a template and some source data. Its main use is in generating configurations for CLI based devices like Cisco routers and switches. It aims to be portable and platform independent through the use of Java and as such requires a Java Runtime Environment (JRE) of at least version 7 to be installed.

This document aims to be short as there isn't really a lot to talk about, due to the simplicity of TemplateFx and its hopeful ease of use. However, if you feel something could be made easier or simpler then feedback is welcomed using the email address above.

Download links for TemplateFx can be found on GitHub at the following location: https://github.com/chrixm/templatefx/releases
All the changes in this release and previous releases can be found within the detailed CHANGELOG.

2. User Interface

The TemplateFx user interface consists of the "Console", "IP Calc" and "DataTemplate" tabs and multiple "Output" tabs. The "Console" tab is where STDOUT/STDERR goes and where all console logging is sent. The "IP Calc" tab provides easy access to an IPv4/IPv6 Subnet Calculator with support for DNS resolution of subnets, the "DataTemplate" tab is the main tab where you compose your template and the "Output" tabs are where you see your merged data after clicking on the "Generate Output" button on the toolbar.

templatefx-screenshot1.png    templatefx-screenshot2.png    templatefx-screenshot3.png

You have the ability to right click on the various panes to allow you to copy and paste or perform other normal operations. As well as the "File" menu to load and save content, you also have the ability to drag and drop data files and template files into the respective panes.

The "Group By" drop down allows you to select one of your data fields to allow grouping of your output data. As an example, if you have configuration that needs to be applied to different interfaces on a series of routers, then you could use this to group by your router identifier, to allow you to group interfaces for the same router in the same output. The "Merge Rows" checkbox is slightly different and is used in conjunction with the "Group By" drop down and it allows you to merge rows together which are grouped together. Using the same example, you would only have a single row per router with all the interfaces merged together using a delimiter.

2.1. Data Pane

The data pane is where you enter your raw data that TemplateFx will use within your template. Text that is inserted into the data pane has a couple of restrictions that must be followed:

  1. Data is entered in rows and columns, with the columns separated by tabs or commas - you can't mix and match. In the same sense if you click on "Import Data" from the "File" menu then it expects tab or comma separated data.

  2. You must define at least two rows within your data. The first row is considered the header row which allows TemplateFx to map the substitution fields in your template to the data. The only exception, is if you haven't used any template fields within the template plane - this is to support situations where you are just using the template pane and in this unusual situation you may leave the data pane blank.

A nice feature of TemplateFx is its ability to accept data from Microsoft Excel. If you construct your data within Excel then you are able to copy and paste it into the data pane. This is the preferred method of constructing source data as you are able to manipulate and sort the data before pasting it into TemplateFx - however be aware of Excel dropping leading zeroes and converting "x/x/x" syntax into dates by default.

NOTE You will get a warning message if the data pane contains a ", ' or \ character. This is because TemplateFx doesn't keep track in which context the template fields are being inserted - it could be within the main body of the template or within a dynamic script block within an already quoted string. In some contexts, those characters may need to be escaped (e.g. <? print("<<FIELD>>") ?> when there is already a double quote in the value of <<FIELD>>), and in some they might not. If you aren't using those template fields in a scenario (generally a script block) which might cause a problem then you can ignore this warning.

2.2. Template Pane

The template pane is where you enter the text of your template that will be used as the basis for your output. TemplateFx fields are defined through the use of double chevrons (e.g. <<FIELD>>) and once you define them within the template pane, they are added to the "Template Fields" list (fields are highlighted in red if they don't exist in the data pane).

When you click on the "Generate Output" button, the fields will be substituted for the values in the source data. In the "Generate Output" dialog you have the option of specifying a label for the output tab as well as some optional criteria which allows you to limit which entries within the data pane are processed. When limiting which entries are processed you can select rows where a field "contains (=~)", "does not contain (!~)", "equals (==)" or "does not equal (!=)" a certain string, with the "contains" and "does not contain" operands supporting a regular expression.

In the event that a template field hasn't been defined within the source data then you will get a warning message as well as it being highlighted in the output to allow you to easily identify it.

2.3. Output Pane

Every time you click on the "Generate Output" button a new output pane is added as a new tab. This pane allows you to see the merged output with the ability to save it or to zip multiple outputs together into a single archive. If you have selected a field in the "Group By" drop down, then you have the ability to cycle through the different output groups. In the event that TemplateFx detects you are creating a HTML based template then it will give you the option of showing the output in your default browser using the "In Browser" button.

You also have various different selection options ("Select Line", "Select Block" and "Select Paragraph") by right clicking on the output pane which allows you to easily select blocks to copy. This goes one step further with a feature called "Copy Mode", which is available in the same menu that automatically highlights blocks as you move the mouse over them - this allows you to copy blocks without having to manually select them first.

2.4. Shortcut Keys

TemplateFx defines the following shortcut/accelerator keys that can be used to perform tasks using the keyboard:

Action Windows Mac OS
New ^N ⌘N
Load DataTemplate ^L ⌘L
Save DataTemplate ^S ⌘S
Find and Replace ^F ⌘F
Bookmark DataTemplate ^D ⌘D
IP Calculator ^I ⌘I
Show DataTemplate Pane ^T ⌘T
Generate Output ^G ⌘G
JavaScript Libraries ^J ⌘J
Close Tab ^W ⌘W
Help F1 F1

2.5. Command Line Options

/M

This option minimises TemplateFx on start-up and doesn't steal the focus - this allows TemplateFx to load in the background without interrupting what the user is doing.

3. DataTemplates

A DataTemplate is a proprietary file format which uses the file extension ".dt" and is used to store a template along with the source data. Instead of having to distribute the template and source data in separate files, you can save it as a DataTemplate, which combines them both together. Warning: The file format isn't text based and if you attempt to edit it outside of TemplateFx then you will screw up the checksum and it won't load.

However, you do have the option to export a DataTemplate (and import) in plain text - this will include all aspects of the DataTemplate using specific section blocks to differentiate different aspects. This allows you to edit it outside of TemplateFx and then import it later on. On import you don't need to include all sections (e.g. you can have a file with just some "snippet" blocks), which allows you to merge in additional sections into an existing DataTemplate - on import you have the option to either "Replace" (clear everything first) or "Merge" (will overwrite elements that already exist) the contents of the existing DataTemplate. Finally, if you paste in an exported DataTemplate to one of the panes, it will detect the syntax and ask you whether you wish to treat it as a DataTemplate or just text.

The DataTemplate is exported in the following text based format:

<templatefx:data>
DATA
</templatefx:data>

<templatefx:group_by = FIELD />
<templatefx:merge_rows = 0 || 1 />
      
<templatefx:snippet = NAME_1>
SNIPPET 1
</templatefx:snippet>

<templatefx:snippet = NAME_2>
SNIPPET 2
</templatefx:snippet>

<templatefx:template>
TEMPLATE
</templatefx:template>

TemplateFx also supports encrypted DataTemplates by selecting "Encrypt DataTemplate" from the "File" menu. After prompting for a password, the DataTemplate will be fully encrypted when it is saved using AES (Advanced Encryption Standard) encryption in CTR mode using a pseudorandom 16 byte IV and a SHA-256 HMAC for integrity which is performed over the salt, IV and ciphertext. The key size is 128 bits (or you can select 256 bits if you have installed the "Java Cryptography Extension Unlimited Strength Jurisdiction Policy Files") and is generated by combining a pseudorandom 64 byte salt with the password using the PBKDF2 (HMAC-SHA-512) key derivation function with 50,000 iterations. Please ensure you don't forget the password as there is no way to decrypt the DataTemplate without it!

4. Templates

To help demonstrate how a template is constructed let us assume we have the following source data defined within our data pane:

ROUTER, INTERFACE, IP, DEST
PE-1A, Gi0/0/0, 192.0.2.1, R1 (Gi0/0)
PE-1A, Gi0/0/1, 192.0.2.5, R2 (Gi0/1)
PE-1A, Gi0/0/2, 192.0.2.9, R3 (Gi0/2)

We are going to create a template which configures a series of interfaces based on the data provided within the data pane. You can see in the example below that we have used chevrons to mark where we want the substituted data to be placed:

interface <<INTERFACE>>
 description ## Connection from <<ROUTER>> (<<INTERFACE>>) to <<DEST>> ##
 ip address <<IP>> 255.255.255.252
 no shutdown
!

Once you click on "Generate Output", the data within the template pane is effectively duplicated for each line within your source data with the necessary substitutions taking place - it will produce the following output:

interface Gi0/0/0
 description ## Connection from PE-1A (Gi0/0/0) to R1 (Gi0/0) ##
 ip address 192.0.2.1 255.255.255.252
 no shutdown
!

interface Gi0/0/1
 description ## Connection from PE-1A (Gi0/0/1) to R2 (Gi0/1) ##
 ip address 192.0.2.5 255.255.255.252
 no shutdown
!

interface Gi0/0/2
 description ## Connection from PE-1A (Gi0/0/2) to R3 (Gi0/2) ##
 ip address 192.0.2.9 255.255.255.252
 no shutdown
!

4.1. Branching and Looping Constructs

4.1.1. IF

The IF construct is one of the most important features of many languages, templates included. It allows for the conditional use of template text based on a true or false statement. If the statement evaluates to true, the text will be displayed, and if it evaluates to false, it won't.

<IF ("expression")>
  ...
<ELSE IF ("expression")>
  ...
<ELSE IF ("expression")>
  ...
<ELSE>
  ...
</IF>

The "expression" will be evaluated by the JavaScript parser, so it needs to be valid JavaScript, however the syntax is relatively simple as demonstrated in the example below (white space is maintained at the beginning and end of IF blocks, but not empty lines).

interface <<INTERFACE>>
<IF ("<<INTERFACE>>" == "Gi0/0/0")>
 no shutdown
</IF>
!

In the above example, the block will only be displayed if the "INTERFACE" field equals "Gi0/0/0".

4.1.2. FOR

The FOR construct is also an important feature of many languages, templates included - it is used to support intra-row looping. There are two different syntaxes that are supported for this construct; the first allows you to specify a variable "VAR" and a comma separated list of data values, e.g. "VAL1", "VAL2", ..., "VALn", the second allows you to specify a variable "VAR" and then use the X TO Y [STEP N] syntax for the values - if "STEP" is omitted then a default value of 1 or -1 will be used depending on the context. It will then duplicate the contents of the FOR block for every entry by substituting {{VAR}} for the value. If there are no entries within the list of data values then the block isn't displayed (white space is maintained at the beginning and end of FOR blocks, but not empty lines). To maintain performance and preserve memory, a maximum iteration limit of 1024 has been imposed.

<FOR VAR = ["VAL1", "VAL2", ..., "VALn"]>
{{VAR}}
</FOR>

The following example demonstrates how this could be used to include a series of "ip helper-address" statements on an interface:

interface <<INTERFACE>>
 ip address <<IP>> 255.255.255.252
<FOR H = ["192.0.2.129", "192.0.2.130"]>
 ip helper-address {{H}}
</FOR>
 no shutdown
!

Once you click on "Generate Output", the following output will be generated where you can see the "ip helper-address" has been duplicated for each entry within the list of data values:

interface Gi0/0/0
 ip address 192.0.2.1 255.255.255.252
 ip helper-address 192.0.2.129
 ip helper-address 192.0.2.130
 no shutdown
!

interface Gi0/0/1
 ip address 192.0.2.5 255.255.255.252
 ip helper-address 192.0.2.129
 ip helper-address 192.0.2.130
 no shutdown
!

interface Gi0/0/2
 ip address 192.0.2.9 255.255.255.252
 ip helper-address 192.0.2.129
 ip helper-address 192.0.2.130
 no shutdown
!

It should be noted that the above could have been easily done without the use of a FOR block as the values were already known. However, the real power comes when you provide the list from the data pane, where it could be different on a per line basis - this concept will be further explained within the "Scripting" section.

The second type of syntax allows you to do more conventional loops that go from a start value to an end value, with an optional step (where "X" and "Y" could be a number or a character with the smaller value on the left or the right):

<FOR VAR = ["X" TO "Y" STEP N]>
{{VAR}}
</FOR>

You are permitted to nest FOR blocks as many times as you want while mixing and matching the syntax - the following primitive example demonstrates an example of this:

<FOR I = [1 TO 5 STEP 2]>
  <FOR J = [B TO A]>
R = {{I}}:{{J}}
  </FOR>
</FOR>

Once you click on "Generate Output", the following output will be generated:

R = 1:B
R = 1:A
R = 3:B
R = 3:A
R = 5:B
R = 5:A

4.2. Snippets

To allow the re-using of templating components, TemplateFx also supports the notion of including the contents of snippets into a template. This allows a common piece of text to be written once but used multiple times in the same template - any modifications to the common snippet is seen in all occurrences within the template. Snippets are defined in the snippet panel and can then be referenced in the main template using the following syntax:

ipv4 access-list ACL-IN
 permit tcp host 192.0.2.1 gt 1023 host 192.0.2.2 eq 179
 permit tcp host 192.0.2.2 eq 179 host 192.0.2.1 gt 1023
 %{ACL-STANDARD}%
 deny ip any any
!

The above %{NAME}% syntax tells TemplateFx to replace it with the contents of the snippet named "ACL-STANDARD". This directive is processed first before any substitutions have taken place which means any valid template syntax can be used within a snippet. The scope of snippets is within the DataTemplate and when the DataTemplate is saved or loaded then the snippets are included with it.

Snippets also support parsing of an optional parameter by specifying parenthesis after the snippet name (i.e. %{NAME("<string>")}%) when you reference it in your template, which can then be accessed in the snippet via the {%} markup. Any occurrences of {%} in the snippet will be replaced with "<string>" - this could be used to pass through a literal string which allows a different response to be produced depending on what is passed through. However, care should be taken when passing through special constructs (i.e. "<?= ?>") as they will be evaluated as part of the snippet body and thus need to be valid in the context they are being evaluated.

4.3. Including External Files

As well as snippets, TemplateFx also supports including content from external text files that are located on a locally accessible file system. This approach might be used over snippets if you have content which is shared amongst multiple templates. External files are included using the following syntax:

%INCLUDE "\\uk\ukroot\templates\site-build-template.txt"

The above syntax instructs TemplateFx to include the contents of "\\uk\ukroot\templates\site-build-template.txt" into the template at this point. The current order of operation means that snippets are imported into the template before includes, which means snippets can include external files. A maximum filesize of 262,144 bytes is currently enforced per external file.

4.4. Comments

There are two types of comments which can be used within a template (and snippets), both of which must be used at the beginning of a line as TemplateFx doesn't presently support end of line comments. The first is a template comment and uses the # syntax to comment out a line in a template - this results in the line being completely ignored and not appearing in your output (if you require a # at the start of a line then you can escape it using ## instead). The second type of comment, which uses the ! syntax is used to supplement your output with comments:

# This comment won't appear in your output and ignores <<FIELDS>>, etc.
! This comment will appear in your output and doesn't ignore <<FIELDS>>, etc.

You can also indicate that you want to highlight the entire row, which is done by using #- and !- respectively:

#- Using a hyphen (-) after the comment symbol will highlight the entire row.

5. Grouping and Merging

There are three features which allow you to control how the outputs are generated: "Group By", "Merge Rows" and Advanced Dynamic Grouping. By default TemplateFx will generate a single output with each row's output one after another. To demonstrate the difference between these features, we are going to use the following data source as an example:

ROUTER, INTERFACE
UKPE-ABC-01, Gi0/1
UKPE-ABC-01, Gi0/2
UKPE-ABC-01, Gi0/3
UKPE-ABC-02, Gi0/1
UKPE-ABC-02, Gi0/2
UKPE-ABC-03, Gi0/1

With none of the features used, we will end up with one large output, with the template output for each of the 6 rows. If we select "ROUTER" as our "Group By" field then TemplateFx will generate outputs in the same way as before, but it will combine outputs with the same value for "ROUTER" into a single output - this will result in 3 separate outputs (one per unique "ROUTER" value). As it is processing each row, it basically checks to see if we have generated an output with the same group value before, if we have then it appends it to the same output and if not then it starts a new output.

The Advanced Dynamic Grouping feature allows you to expand rows into multiple outputs by defining different outputs in the same template as well as using dynamic values for group names. Let's take the following template as an example:

<GROUP = <?= "<<ROUTER>>-A".toLowerCase() ?>>
interface <<INTERFACE>>
 shutdown
!
</GROUP>

<GROUP = <<ROUTER>>-B>
interface <<INTERFACE>>
 shutdown
!
</GROUP>

The above example will result in two outputs for each unique device (six in total) - one for the "-A" device based on the ROUTER column (but dynamically converted to lowercase first) and one for the "-B" device, also derived from the "ROUTER" column. Anything outside of those GROUP sections will remain in the original output - the purpose of GROUP sections is to move output from the original output into a new output. TemplateFx doesn't support nested GROUP sections as they don't make a lot of sense.

The "Merge Rows" feature goes a lot further and actually combines rows together in your data source where the "Group By" field has the same value. Using the example above, TemplateFx would merge rows where the "ROUTER" field was the same, which would result in 3 rows within our data source (any optional criteria specified in the "Generate Output" dialogue is processed before merging). When the rows are merged, TemplateFx will pick a delimiter (from a pre-defined list, which includes "|;:%$?^+-_") to separate the different rows - the first delimiter which it doesn't find in your data source is used (the actual value is accessible using the templatefx.delimiter internal variable, which is set to "null" if "Merge Rows" is disabled). Based on the above example, the merged data source would look like this:

ROUTER, INTERFACE
UKPE-ABC-01|UKPE-ABC-01|UKPE-ABC-01, Gi0/1|Gi0/2|Gi0/3
UKPE-ABC-02|UKPE-ABC-02, Gi0/1|Gi0/2
UKPE-ABC-03, Gi0/1

If you were to use the <<ROUTER>> field in your template then you would end up with the value "UKPE-ABC-01|UKPE-ABC-01|UKPE-ABC-01" being substituted in your output. You have two options, you can either use JavaScript to split out the different values manually using templatefx.delimiter, or you can use a TemplateFx function to assist you. To support the "Merge Rows" feature, TemplateFx now supports the templatefx.mrows internal variable which tells you how many rows have been merged into the current row and the <<ROUTER>>[n] syntax, where "n" is the index to the merged row. This syntax is actually a shortcut function and will get substitued for JavaScript during pre-processing, which splits the data on the delimiter, places it into a temporary array and then returns the array element at position "n".

However, there is a big difference between <<ROUTER>> and <<ROUTER>>[n], with the first being substituted for a string literal and the second being substituted for a JavaScript function - this is an important difference when they are being used in the context of a script block:

# In this context, <<FIELD>> must be contained within quotes as it will be substituted for a string literal.
<?= ip("<<FIELD>>", +1) ?>

# In this context, <<FIELD>>[n] must not be contained within quotes as it will be substituted for a function.
<?= ip(<<FIELD>>[n], +1) ?>

With the confusing bit out of the way, the following hopefully demonstrates how you can elevate your templates by using "Merge Rows" to provide looping within a single output. The following example generates an output per router, but it uses the different interfaces contained on different rows within the configuration of a single router (we are using templatefx.mrows to loop through the different merged rows):

! @ <<ROUTER>>[0]
router isis
<FOR I = [0 TO <?= templatefx.mrows - 1 ?>]>
 interface <<INTERFACE>>[{{I}}]
  bfd fast-detect ipv4
 !
</FOR>
!

With "Merge Rows" enabled, this would result in the following output, with each interface being included in the same output:

! @ UKPE-ABC-01
router isis
 interface Gi0/1
  bfd fast-detect ipv4
 !
 interface Gi0/2
  bfd fast-detect ipv4
 !
 interface Gi0/3
  bfd fast-detect ipv4
 !
!


! @ UKPE-ABC-02
router isis
 interface Gi0/1
  bfd fast-detect ipv4
 !
 interface Gi0/2
  bfd fast-detect ipv4
 !
! 


! @ UKPE-ABC-03
router isis
 interface Gi0/1
  bfd fast-detect ipv4
 !
!

It should be noted that even without "Merge Rows" enabled, templatefx.mrows would still return "1" and the syntax <<FIELD>>[0] is still valid and would return the same value as <<FIELD>>. This is useful as it allows you to turn off the merging without changing your template. If we were to disable "Merge Rows" using the same data and template from the example above, we would end up with the following output instead:

! @ UKPE-ABC-01
router isis
 interface Gi0/1
  bfd fast-detect ipv4
 !
!

! @ UKPE-ABC-01
router isis
 interface Gi0/2
  bfd fast-detect ipv4
 !
!

! @ UKPE-ABC-01
router isis
 interface Gi0/3
  bfd fast-detect ipv4
 !
!


! @ UKPE-ABC-02
router isis
 interface Gi0/1
  bfd fast-detect ipv4
 !
!

...

It should also be noted that neither of these features modify the order of the data source, the data is processed in the order provided - any sorting should be done before the data is entered into TemplateFx.

6. Scripting

For the more advanced users who want to do more than just simple substitution, TemplateFx has the ability to interpret dynamic templates which are written using JavaScript. This User Guide assumes you have a basic understanding of JavaScript, but if this isn't the case then the web (http://lmgtfy.com/?q=JavaScript) has a large amount of information on it.

TemplateFx supports the following different syntax within a template to support dynamic content:

<? ... ?>

This is used to define a dynamic content block where the contents of the block between the <? ... ?> will be evaluated through the JavaScript parser and any output will be substituted. For the purpose of this example we are going to modify our source data to demonstrate how we can use dynamic content to generate BGP configurations:

ROUTER, LOCATION, INTERFACE, PEER_IP
PEXCD1A, DEV, Gi0/0/0/1, 192.0.2.2
PEXCD1B, DEV, Gi0/0/0/1, 192.0.2.6
PEXAY1A, PROD, Te0/7/0/1, 192.0.2.66
PEXAY1B, PROD, Te0/7/0/1, 192.0.2.70

The below example will demonstrate how to generate some BGP configuration from the source data above. It will determine the BGP AS (Autonomous System) number based on the "LOCATION" field as well as working out the suffix for the RD (Route Distinguisher) based on the "ROUTER" field (it will look to see if the router name ends in "A" or not to determine the correct suffix):

! <<ROUTER>>
<?
 var BGP_AS = ("<<LOCATION>>" == "DEV") ? "64512" : "37119";
 var RD_SUFFIX = ("<<ROUTER>>".match(/A$/)) ? "01" : "02";
?>

router bgp <? print(BGP_AS) ?>
 vrf TEST
  rd <? print(BGP_AS) ?>:100<? print(RD_SUFFIX) ?>
  address-family ipv4 unicast
  !
  neighbor <<PEER_IP>>
   remote-as 65000
   address-family ipv4 unicast
    maximum-prefix 100 80 warning-only
    soft-reconfiguration inbound always
   !
  !
 !
!

Once you submit the data, it will produce the following output:

! PEXCD1A

router bgp 64512
 vrf TEST
  rd 64512:10001
  address-family ipv4 unicast
  !
  neighbor 192.0.2.2
   remote-as 65000
   address-family ipv4 unicast
    maximum-prefix 100 80 warning-only
    soft-reconfiguration inbound always
   !
  !
 !
!


! PEXCD1B

router bgp 64512
 vrf TEST
  rd 64512:10002
  address-family ipv4 unicast
  !
  neighbor 192.0.2.6
   remote-as 65000
   address-family ipv4 unicast
    maximum-prefix 100 80 warning-only
    soft-reconfiguration inbound always
   !
  !
 !
!


! PEXAY1A

router bgp 37119
 vrf TEST
  rd 37119:10001
  address-family ipv4 unicast
  !
  neighbor 192.0.2.66
   remote-as 65000
   address-family ipv4 unicast
    maximum-prefix 100 80 warning-only
    soft-reconfiguration inbound always
   !
  !
 !
!


! PEXAY1B

router bgp 37119
 vrf TEST
  rd 37119:10002
  address-family ipv4 unicast
  !
  neighbor 192.0.2.70
   remote-as 65000
   address-family ipv4 unicast
    maximum-prefix 100 80 warning-only
    soft-reconfiguration inbound always
   !
  !
 !
!
<?= ... ?>

This is used as shorthand to <? print(...) ?>, where the <?= is used to tell TemplateFx that we want to use the value of the variable. We could write some of the example above using the following syntax instead of the print statements:

router bgp <?= BGP_AS ?>
 vrf TEST
  rd <?= BGP_AS ?>:100<?= RD_SUFFIX ?>
 !
!

To demonstrate some of the more powerful uses of this construct, if we revisit our FOR example above and see how we can update it to allow the user to provide the list of values within the source data. For the purpose of this example we have updated our source data to include an additional column called "HELPERS" as well as changing our "IP" field to the base "IP_SUBNET":

ROUTER, INTERFACE, IP_SUBNET, DEST, HELPERS
PE-1A, Gi0/0/0, 192.0.2.0, R1 (Gi0/0), 192.0.2.129; 192.0.2.130; 192.0.2.131
PE-1A, Gi0/0/1, 192.0.2.4, R2 (Gi0/1)
PE-1A, Gi0/0/2, 192.0.2.8, R3 (Gi0/2), 10.254.0.1

If we now update our original template to use the <?= ... ?> syntax we can see how this becomes a much more powerful construct:

interface <<INTERFACE>>
 ip address <?= ip("<<IP_SUBNET>>", +1) ?> 255.255.255.252
<FOR H = [<?= "<<HELPERS>>".split("; ") ?>]>
 ip helper-address {{H}}
</FOR>
 no shutdown
!

First off we are now using the built-in ip() function to derive the IP address of the interface - the ip() function is being used to add a single IP address to the base value. The JavaScript split() function is being used to derive the list of data from a variable provided within the data set. If we click on "Generate Output" we will get the following output:

interface Gi0/0/0
 ip address 192.0.2.1 255.255.255.252
 ip helper-address 192.0.2.129
 ip helper-address 192.0.2.130
 ip helper-address 192.0.2.131
 no shutdown
!

interface Gi0/0/1
 ip address 192.0.2.5 255.255.255.252
 no shutdown
!

interface Gi0/0/2
 ip address 192.0.2.9 255.255.255.252
 ip helper-address 10.254.0.1
 no shutdown
!

WARNING The JavaScript parser allows you to reference Java classes (e.g. java.math.BigInteger) from within JavaScript - this is called Live Connect. Although JavaScript doesn't permit access to the local file system, Java does and could allow a malicious user to send you a template that did naughty things - this is the same principal as someone sending you an Excel sheet with a malicious macro. You need to be aware of this and be cautious if you ever receive a template or DataTemplate from someone you don't know or trust.

6.1. Built-In JavaScript Variables

As well as the standard JavaScript syntax there is also a series of built-in variables that are included with TemplateFx. These will be expanded with future versions of TemplateFx, but for the time being here is the current list:

templatefx.version

This variable returns the current TemplateFx version as a string (e.g. "2.38").

templatefx.build

This variable returns the current TemplateFx build number as a string (e.g. "14222").

templatefx.jre_version

This variable returns the current Java (JRE) version as a string (e.g. "1.7.0_79").

templatefx.group_start

This variable will be set to true if the row is the first in the group. This allows you to add text to your template that would be included at the beginning of every output. An example is included below:

<IF (templatefx.group_start)>
!-----
! @ <<ROUTER>>
!-----
</IF>

NOTE This is only applicable for "Group By" groups and doesn't work with Advanced Dynamic Grouping where GROUP sections have been added to the template. In this scenario you could use the counter() function as follows:

<GROUP = "<<ROUTER>>">
<IF (counter ("<<ROUTER>>") == 1)>
! @ <<ROUTER>>
</IF>
DATA
</GROUP>
templatefx.group_end

This variable is the opposite of the templatefx.group_start where it will be set to true if the row is the last in the group. This allows you to add text to your template that would be included at the end of every output.

NOTE This is only applicable for "Group By" groups and doesn't work with Advanced Dynamic Grouping where GROUP sections have been added to the template.

templatefx.row

This variable returns the current row within the data set.

templatefx.rows

This variable returns the total amount of rows within the data set.

templatefx.mrows

When using "Merge Rows", this variable returns the number of rows merged into the current row of the data set.

templatefx.delimiter

When using "Merge Rows", this variable returns the character which is used as the delimiter between merged rows.

templatefx.output

This variable contains the output from the template as it is being generated - as the template is processed, the contents of this variable will dynamically change. If you check this variable at the end of your template then it will contain the finished output. This is useful if you want to validate anything in the output (i.e. search through the output and find all the IP addresses to check they all have a valid host entry in DNS). Please refrain from trying to update the output using this variable as bad things could happen - please see the add_output_mutation() function for changing the output post generation.

templatefx.fields["field"]([index])

This variable is a JavaScript array of functions and thus must be called with parenthesis at the end when accessing array members as they are functions - it provides an alternative (advanced) method of accessing the field values instead of using the <<FIELD>> syntax, however all "field" values must be passed as uppercase. This has been added for a specific use-case as there was no way to access a field value dynamically, the <<FIELD>> syntax doesn't support any variable or script section between the chevrons (i.e. <<{{F}}>>). To demonstrate this, the below example loops through all the fields in the header row and queries the specific field values of the row you are processing by dynamically passing the field name using the FOR looping variable {{F}}.

<FOR F = [<?= Object.keys(templatefx.fields) ?>]>
{{F}} has a value of <?= templatefx.fields["{{F}}"]() ?> - equivalent of <<{{F}}>> which isn't supported.
</FOR>

The templatefx.fields variable is commonly referred to as an associative array (sometimes referred to as a Hash or Map in other languages) - it allows you to access array values via the syntax value = array["key"]() - the parenthesis are required as each value is actually a function in this instance. An associative array is an Object with "keys" (the field names) and "values" (the field values - or functions in this case) so we have to use Object.keys() to loop through the keys of the array.

To help in explaining this, the following data source will be used as an example:

ROUTER, LOCATION, INTERFACE, PEER_IP
PEXAY1A, PROD, Te0/7/0/1, 192.0.2.66
PEXAY1B, PROD, Te0/7/0/1, 192.0.2.70

By combining the above data source and template together it would result in the following output, which has an entry per header field per row:

ROUTER has a value of PEXAY1A
LOCATION has a value of PROD
INTERFACE has a value of Te0/7/0/1
PEER_IP has a value of 192.0.2.66

ROUTER has a value of PEXAY1B
LOCATION has a value of PROD
INTERFACE has a value of Te0/7/0/1
PEER_IP has a value of 192.0.2.70

The other thing worth mentioning is how this method can be used in combination with "Merge Rows" - where multiple rows are combined into a single row and values are separated by a delimiter. In the same way that the <<FIELD>>[x] shortcut syntax can be used to access an individual merged value, we can do the same with templatefx.fields as demonstrated below.

<FOR F = [<?= Object.keys(templatefx.fields) ?>]>
{{F}} has the following values:
  <FOR I = [0 TO <?= templatefx.mrows - 1 ?>]>
<?= templatefx.fields["{{F}}"]({{I}}) ?> - equivalent of <<{{F}}>>[{{I}}] which isn't supported.
  </FOR>
</FOR>

6.2. Built-In JavaScript Functions

In addition to the built-in variables, there is also a series of built-in functions that are included with TemplateFx. These will be expanded with future versions of TemplateFx, but for the time being here is the current list:

counter (["key"], [increment], [start])

This function provides a counter which will produce a series of numbers in the given sequence. If the "key" is omitted or passed as "null" then the counter is reset back to 0 (or "start") for every row. If a "key" is provided then the counter is persistent across rows. Every time you call the function with the same "key" then it will increment the counter and return the number. Different persistent counters can be created using different "key" values. There is an optional "increment" parameter which allows you to specify a different increment as opposed to the default of 1. There is also an optional "start" parameter which allows you to specify a different start number as opposed to the default of 1. The following example demonstrates both of these parameters to produce a positive and negative counter:

# This will produce a positive sequence of 0, 5, 10, 15, 20, etc - it will persist across rows using key "id"
<?= counter("id", 5, 0) ?>

# This will produce a negative sequence of 0, -5, -10, -15, -20, etc - it will be reset to 0 for every row
<?= counter(null, -5, 0) ?>

# This will produce a positive sequence of 1, 2, 3, 4, 5, etc - it will be reset to 1 for every row
<?= counter() ?>
cancel (["message"])

This function provides a way to cancel processing a template if something isn't right. This could be if an invalid value has been specified in the data source or if a key field hasn't been filled in correctly. An optional "message" can be provided to give the user more information about why processing was cancelled. The first time the function is executed, the whole processing is stopped.

ip ("ip", number, [ipv6format]) IPv4 + IPv6

This function takes an IPv4 or IPv6 address (doesn't support IPv4-mapped IPv6 addresses) and adds or subtracts a number of IP addresses from the value. If it is an IPv6 address then you can specify an optional third parameter which allows you to specify the presentation format of the returned IPv6 address, using one of the following values:

ipv6format = 0 || null
This, or the default if omitted, will output the address using the RFC4291 preferred format of "x:x:x:x:x:x:x:x" (i.e. 2001:db8:0:0:8:800:200c:417a), where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of the address with leading zeroes omitted.

ipv6format = 1
This will output the address using a special compressed format (i.e. 2001:db8::8:800:200c:417a), which allows you to replace long strings of zero bits with "::". The use of "::" indicates one or more groups of 16 bits of zeroes. The "::" can only appear once in an address and should be the longest run of groups of zeroes. The "::" can also be used to compress leading or trailing zeroes in an address.

ipv6format = 2
This will output the address using the full expanded format (i.e. 2001:0db8:0000:0000:0008:0800:200c:417a).

To demonstrate this function, some examples are provided below:

# Obtain the IP address before "192.0.2.10" (i.e. 192.0.2.9)
<?= ip("192.0.2.10", -1) ?>

# Convert the IPv6 address "2001:0db8:0000:0000:0008:0800:200c:417a" into compressed form (i.e. 2001:db8::8:800:200c:417a)
<?= ip("2001:0db8:0000:0000:0008:0800:200c:417a", 0, 1) ?>

# Add 32 to the IPv6 address "2001:db8::" and output in preferred form (i.e. 2001:db8:0:0:0:0:0:20)
<?= ip("2001:db8::", 32) ?>
insubnet ("subnet", "ip") IPv4 + IPv6

This function takes a subnet in CIDR format (i.e. "address/mask") and returns true if the passed IPv4 or IPv6 address is in the subnet or false if it isn't. The following example verifies if "2001:db8::1" is contained within "2001:db8::/32":

<IF (insubnet("2001:db8::/32", "2001:db8::1"))>
  ...
</IF>
ipsplit ("subnet", "cidr_mask" | min_subnets) IPv4 + IPv6

This function is used to split up a subnet into smaller subnets - the subnet parameter is provided in CIDR format (i.e. "192.0.2.0/24" or "2001:db8::/32") and the second parameter can be specified in one of two ways. The first way is by specifying a CIDR mask of the smaller subnets (e.g. "/mask") and the second is by specifying the minimum number of subnets you want (this number will be rounded up to the nearest power of 2 - e.g. 5 will become 8). The function will return "null" if an error occurs, the string "{mask overflow}" if you have used too many bits between the masks (you can't have more than 10 bits between subnet and mask), "{number overflow}" if you have used too many bits when specifying a minimum number of subnets, or an array of all the subnets on success. The following example will output all the "/29" subnets in "192.0.2.0/26" (i.e. 8 subnets) - one per row as we join the returned array using a newline character:

<?= ipsplit("192.0.2.0/26", "/29").join("\n") ?>

To demonstrate the same by specifying the minimum number of subnets, we can specify 8 as the second parameter, but using an IPv6 address this time - this will return "/35" subnets as there are 8 of them to a "/32" subnet:

<?= ipsplit("2001:db8::/32", 8).join("\n") ?>

To be able to split a subnet into smaller subnets the "cidr_mask" should be longer than the mask on the "subnet" (and have 10 bits or less between them). However, if you specify a shorter mask as your "cidr_mask" then it will return the supernet instead (i.e. using "192.0.2.0/24" with a "cidr_mask" of "/16" will return "192.0.0.0/16"). In the same sense you can specify a negative number to provide a supernet (i.e. using "192.0.2.0/28" with the minimum number of subnets as -8 will return "192.0.2.0/25").

range2cidr ("range") IPv4 + IPv6

This function is used to convert an IP address range into the smallest number of CIDR subnets to cover the entire range. The "range" parameter specifies a start and end address (e.g. "192.0.2.1-192.0.2.137" or "2001:db8::-2001:db8::ABC") and will return an array of subnets or null if you have tried to do something stupid. The following example will output all the subnets between "192.0.2.87" and "192.0.2.177" - one per row as we join the returned array using a newline character:

<?= range2cidr("192.0.2.87-192.0.2.177").join("\n") ?>

This results in the following output:

192.0.2.87/32
192.0.2.88/29
192.0.2.96/27
192.0.2.128/27
192.0.2.160/28
192.0.2.176/31
network ("cidr") IPv4 + IPv6

This function takes an IPv4 or IPv6 prefix in CIDR format (i.e. "192.0.2.13/24" or "2001:db8::8:800:200c:417a/64") and returns the network address. The function will return "null" if an error occurs, an invalid parameter is detected. In the above example it will return the string "192.0.2.0" for IPv4 and "2001:db8:0:0:0:0:0:0" for IPv6.

ipfirst ("cidr") IPv4 + IPv6

This function takes an IPv4 or IPv6 prefix in CIDR format (i.e. "192.0.2.13/24" or "2001:db8::8:800:200c:417a/64") and returns the first usable address of the subnet. The function will return "null" if an error occurs or an invalid parameter is detected.

iphosts ("cidr", [nwbc]) IPv4 + IPv6

This function takes an IPv4 or IPv6 prefix in CIDR format (i.e. "192.0.2.13/24" or "2001:db8::8:800:200c:417a/64") and returns (as an array) all the usable addresses of the subnet. The function will return "null" if an error occurs or an invalid parameter is detected. By default it doesn't return the "network" and "broadcast" address of IPv4 subnets, but there is an optional parameter "nwbc", which if set to "true" will return them.

iplast ("cidr") IPv4 + IPv6

This function takes an IPv4 or IPv6 prefix in CIDR format (i.e. "192.0.2.13/24" or "2001:db8::8:800:200c:417a/64") and returns the last usable address of the subnet. The function will return "null" if an error occurs or an invalid parameter is detected.

broadcast ("cidr") IPv4 Only

This function takes an IPv4 prefix (there is no concept of a broadcast in IPv6) in CIDR format (i.e. "192.0.2.13/24") and returns the broadcast address. The function will return "null" if an error occurs, an invalid parameter is detected or if the mask length doesn't support a broadcast address (i.e. /31 or /32).

smask ("cidr", [inverse]) IPv4 + IPv6

This function takes an IPv4 or IPv6 prefix in CIDR format (i.e. "192.0.2.13/24" or "2001:db8::8:800:200c:417a/64") and returns the subnet mask (e.g. "255.255.255.0" or "ffff:ffff:ffff:ffff::") or for IPv4 addresses only, the inverse/wildcard mask (e.g. "0.0.0.255") if the optional parameter "inverse" is true. The function will return "null" if an error occurs or if an invalid parameter is detected.

ip2long ("ip") IPv4 Only

This function takes an IPv4 address and converts it into a long.

long2ip (long) IPv4 Only

This function takes a long and converts it into an IPv4 address.

nslookup ("query", [response_type]) IPv4 + IPv6

This function performs a forward or reverse DNS lookup - for reverse DNS lookups it takes an IPv4 or IPv6 address (doesn't support IPv4-mapped IPv6 addresses), converts it to a PTR record, then attempts to resolve it and returns the hostname as a string on success or null on failure - the "response_type" field is ignored on these type of requests. For forward DNS lookups it takes a hostname and attempts to resolve it (it will follow CNAME records if they exist until it finds an IP address) - this may result in multiple IP addresses so is returned as an array of addresses on success or null on failure (the locally configured DNS search list isn't checked, so you will need to specify the domain part as well). By default it will use the system DNS servers, but it will respect the "Custom Name Servers" field in the "IP Calc" pane, but will ignore the "DNS Search List". The optional second parameter allows you to control what records are returned on forward DNS lookups and can be one of the following values:

response_type = 0 || null
This, or the default if omitted, will return both AAAA and A records (AAAA before A) when performing a forward DNS lookup.

response_type = 1
This will return only A records and omit any AAAA records returned - this may result in a null response if no A records exist.

response_type = 2
This will return only AAAA records and omit any A records returned - this may result in a null response if no AAAA records exist.

In addition to being able to lookup a single address, it also supports being passed a list of IP addresses to resolve, with an array being returned with the results - on failure a single null will be returned. This could be used in the following way to lookup all usable hosts within a subnet by combining it with the iphosts() function (with a result per line):

<?= nslookup(iphosts("192.0.2.0/28")).join("\n") ?>

An alternative way of doing the same thing, but with more control on the output would be using a FOR loop as follows:

<FOR H = [<?= iphosts("192.0.2.0/28") ?>]>
{{H}} = <?= nslookup("{{H}}") ?>
</FOR>
passwd ([length], ["chars"])

This function generates a cryptographically strong random password using the following characters: a-z, A-Z, 0-9, unless a custom character set using the "chars" parameter has been provided. The length is determined by the passed "length" parameter or a length of 16 is used if the parameter is omitted.

# Generate a random 12 character password using the provided characters
<?= passwd(12, "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZZ") ?>
rot13 ("string")

This function encodes (or decodes) a "string" using "rot13". Not to be confused with any sort of encryption as it is a very basic substitution cipher which can be easily reversed. It could be used to encode strings to stop the casual passer-by from gleaning passwords from looking at your screen, if you feel it necessary to hard code them into your templates.

timestamp (["format"])

This function generates a timestamp based on the current date and time, either based on a provided format (using the same format as Java's SimpleDateFormat class) or using a default pattern if omitted.

date ("date", "format", amount, ["dest_format"], ["field"])

This function is used to manipulate dates by adding or subtracting a duration from a given date, or by converting one date format to another. The "date" field is used to pass a particular date string and the "format" field is used to specify the format of that given date (using the same format as Java's SimpleDateFormat class). The "amount" field specifies the number of days (by default) to add or subtract from the given date - to subtract days, a negative number can be provided. On success it will return the manipulated date in the same format that was provided - you can specify a different output format using the "dest_format" optional parameter (using the same formatting rules). By default this function operates using days, but by specifying either "d" for days, "w" for weeks, "m" for months or "y" for years as the "field" parameter, the default behaviour can be changed.

# Add 5 days to 27/04/2016 and output using dd/MMMM/yyyy (i.e. 02 May 2016)
<?= date("27/04/2016", "dd/MM/yyyy", 5, "dd MMMM yyyy") ?>

# Subtract 7 days from 03/JAN/2016 and output using the same format (i.e. 25/Dec/2015)
<?= date("01/Jan/2016", "dd/MMM/yyyy", -7) ?>

# Convert date 29/02/16 from dd/MM/yy to dd/MMM/yyyy (i.e. 29/Feb/2016)
<?= date("29/02/16", "dd/MM/yy", 0, "dd/MMM/yyyy") ?>

# Add 5 weeks to 18/OCT/2014 and output using the same format (i.e. 22/Nov/2014)
<?= date("18/Oct/2014", "dd/MMM/yyyy", 5, null, "w") ?>
add_output_mutation ("regex", "replacement")

This function allows you to make changes to the output post generation before it is displayed. This function can be called at any point in your template, but it won't perform the changes until after the output has finished being generated. The replacements will be performed in the order they are added. This function could be used to insert information into your template that isn't known at the point where you want it inserted. As an example, you might want to add a section to your output which lists all the IP addresses which have been used in your output, however you won't being able to produce this list until after the output has been generated. You could leave a placeholder (i.e. "[PLACEHOLDER]") in the template and then use this function to update it at the end with the list of IP addresses that have been found by searching through the output using templatefx.output.

console.log ("message")

This function is used for console logging to aid troubleshooting. It will write the passed message to the console. This allows you to debug your templates to output the current value of a variable, etc in different places.

6.3. External JavaScript Libraries

In addition to the built-in JavaScript variables and functions, TemplateFx also includes version 3.10.1 of the "lodash.js" JavaScript library. This library is similar to "Underscore.js" and is a utility library delivering extra JavaScript functions for common tasks (using the "_" object) - full details on syntax and usage can be found in their API Documentation - the example below shows the version of the library that has been included is as stated above.

<?= _.VERSION ?>

In addition to including this library in TemplateFx, I have also added support for globally including other JavaScript libraries (and also disabling "lodash.js" if you wish). You can select the "JavaScript Libraries" menu item on the "File" menu to add or remove libraries - these are persistent across application restarts, enabled globally and will be available for every DataTemplate. If you have issues including some JavaScript libraries, I recommended you try with the latest version of the Java Runtime Environment - the JavaScript processing was greatly improved from Java 8 onwards with the Nashorn JavaScript Engine. By default TemplateFx will cache the loading of JavaScript Libraries on the first run (you can disable this by deselecting the "Cache JavaScript Libraries" option) - this is because Java seems to be getting slower and slower at parsing JavaScript with every release and this speeds things up a bit.