Icon

TemplateFx | Dynamic Templating Tool

by Chris Mason <chris@templatefx.org>

Latest Version: 2.62 (Build 17230)
Released: 18th August 2017

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 routers, switches, firewalls, etc. 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.

TemplateFx consists of two separate components - the TemplateFx GUI client and the GUI-less TemplateFx Server which contains just the core templating engine accessible through a REST API. The majority of this document is written with the GUI client in mind as a lot of the templating syntax is the same (where things are only supported in the GUI client they are marked with GUI Only). However, a section has been added at the end which provides some detailed usage around the TemplateFx Server.

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" list allows you to select one or more of your data fields (using CTRL/⌘ while clicking to select or deselect entries) 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 and interface identifier, to allow you to group interfaces for the same router in the same output. There is also support for splitting an output into multiple outputs using advanced GROUP syntax that is explained in the "Grouping and Merging" section.

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.

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 (GUI)

-DoI

If you have passed TemplateFx an exported DataTemplate as well as this parameter then TemplateFx will treat the file as a temporary file and delete it once it has been loaded (DoI = Delete on Import). This is useful in scenarios where another tool is generating a DataTemplate in a textual format using a temporary file and is then launching TemplateFx in an automated way - this parameter allows TemplateFx to clean up the temporary file once it has been loaded.

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.

The DataTemplate is exported in the following text based format:

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

<templatefx:group_by = FIELD_1[ || FIELD_2][ || ...] />
      
<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
<ELSE IF ("<<INTERFACE>>" != "Te0/3/0/7")>
 shutdown
</IF>
!

In the above example, the block will display "no shutdown" if the "INTERFACE" field equals "Gi0/0/0" else "shutdown" if the "INTERFACE" field doesn't equal "Te0/3/0/7". This works fine if you want to do simple matches, but JavaScript also supports regular expressions to provide a much richer matching experience. The JavaScript String.match() function can be used to determine if a string contains or doesn't contain a regular expression:

interface <<INTERFACE>>
<IF ("<<INTERFACE>>".match(/^Gi/i))>
 no shutdown
<ELSE IF (!"<<INTERFACE>>".match(/^Te/i))>
 shutdown
</IF>
!

The above syntax will perform a "no shutdown" on any interface which starts with "Gi" and a "shutdown" on any other interface which doesn't start with "Te" (note the use of ! to negate the expression as well as the use of the /i modifier for case-insensitive matching).

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. There are two Snippets which have a special meaning (~HEADER and ~FOOTER), which are automatically added to either the start or the end of your template - hopefully self-explanatory.

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.

The final use of snippets is a bit of a hack, but allows DataTemplate specific options to be defined and saved within the DataTemplate. The following "special" snippets are currently supported:

4.3. Including External Files

GUI Only Not supported within TemplateFx Server DataTemplates

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. User Input

GUI Only Not supported within TemplateFx Server DataTemplates

There might be some situations where you don't want to store dynamic or sensitive data within the actual DataTemplate. In these situations, TemplateFx provides the ability to request data from the user as the output is being generated through the use of input blocks using the <[ ... ]> or <[ ... ][0|1|2]> syntax. By default it will present the user with a multi-line input box, which allows you to enter data which would normally be difficult to do so using the "Data" pane. The second format of syntax allows a numerical type to be specified as per the list below:

0
This is the default and indicates that a multi-line input box will be presented to the user - the input will be accepted when the user clicks "OK" or presses "<CTRL> + <ENTER>".

1
This indicates a single-line input box will be presented to the user - the input will be accepted when the user clicks "OK" or presses "<ENTER>".

2
This indicates a single-line password input box will be presented to the user - the input will be masked and it will be accepted when the user clicks "OK" or presses "<ENTER>".

# Use Multi-Line Input Box (Implicit Type 0)
<[ Enter Data: ]>

# Use Multi-Line Input Box (Explicit Type 0)
<[ Enter Data: ][0]>

# Use Single-Line Input Box (Explicit Type 1)
<[ Enter Username: ][1]>

# Use Single-Line Password Input Box (Explicit Type 2)
<[ Enter Password: ][2]>

Regardless of how many data rows you have, the user will be prompted once per input before output generation commences (if the same input is requested multiple times then the value is cached). The value provided by the user will then replace the <[ ... ]> block.

4.5. 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. Filtering and Sorting

TemplateFx provides the capability to filter and sort the data before an output is generated. To use this feature you need to create a snippet called ~FILTER and fill it with your filtering and sorting criteria based on the following syntax (case insensitive comparison is performed):

FIELD =~ REGEX    # FIELD CONTAINS REGEX
FIELD !~ REGEX    # FIELD DOESN'T CONTAIN REGEX
FIELD == [VALUE]  # FIELD EQUALS VALUE
FIELD != [VALUE]  # FIELD DOESN'T EQUAL VALUE

Multiple criteria on different lines will be treated as "AND" and multiple criteria on the same line separated by commas will be treated as "OR". As well as filtering, it also supports the ability to sort the data using one or more fields:

SORT FIELD_1[ ASC| DESC][, FIELD_2[ ASC| DESC]][, ...]

By default it will sort the data in ascending order but allows you to specify "ASC" for ascending or "DESC" for descending at the end of every sort field. Multiple sort fields can be specified by adding them onto the same line separated by commas.

A, B, C
1, 1, X
Y, 3, Z
2, 1,
1, F, Y
Z, A, W

Using the above data as an example with the following ~FILTER snippet:

C != 
A =~ [0-9], B =~ [A-Z]
SORT A DESC, C

The above is the equivalent of ("C" doesn't equal "") AND (("A" contains a number) OR ("B" contains a letter)) and then sort by "A" in descending order and then by "C" in ascending order, which results in the following data being used when generating outputs:

A, B, C
Z, A, W
1, 1, X
1, F, Y

It also supports the ability to filter which output groups are shown by using a special "~GROUP" field in the same way - however, you can't mix the "~GROUP" syntax with normal header fields in the same "OR" clause.

6. Grouping

There are two features which allow you to control how the outputs are generated: "Group By" GUI Only 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.

It also supports the ability to change the position of where certain GROUP sections appear in the output by specifying an optional number to determine the order - if we take the following as an example:

<GROUP = OUTPUT>
FIRST
</GROUP>
      
<GROUP = OUTPUT>
SECOND
</GROUP>

If you have multiple rows (e.g. 3) of data then you will end up with the following output:

FIRST
SECOND
FIRST
SECOND
FIRST
SECOND

However, if you use the following instead and specify an order (any positive number starting from 0) for the specific GROUP section - then it will order the sections based on the numbers in ascending order (any GROUP section without a number will assume "0" - which means it will be output first):

<GROUP = OUTPUT>[0]
FIRST
</GROUP>
      
<GROUP = OUTPUT>[1]
SECOND
</GROUP>

This will result in the following output:

FIRST
FIRST
FIRST
SECOND
SECOND
SECOND

7. 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
   !
  !
 !
!

Although JavaScript has no native support for HEREDOC style syntax, TemplateFx does provide limited support for it in script blocks. It uses similar syntax to other languages which do support it (e.g. PHP, Perl, etc). This is possible as TemplateFx parses the JavaScript within a script block before it is evaluated, so it is able to convert it to valid JavaScript beforehand. The following syntax is valid within a script block - the "EOF" marker can be any arbitrary text, but it must be the same at the start and the end.

<?
var x = <<<EOF
This is a
multi-line piece
of text.
EOF;
?>
          
<?= x ?>

The ${var} syntax can also be used to reference existing JavaScript variables, for example:

<?
var sname = "Chris";
var x = <<<EOF
My name is ${sname}
EOF;
?>

<?= x ?>
<?= ... ?>

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. To protect you from this, the Java SecurityManager is now enabled to stop any access to local resources by scripts.

7.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:

globals[]

This variable provides a mechanism to store persistent data across rows - it is an associative array with global scope, which you use by adding elements. The following example demonstrates how we can store the value of the field "XYZ" from the current row as globals["xyz"] so we can reference it when processing the next row.

<?
  if (typeof globals["xyz"] !== "undefined") {
    if ("<<XYZ>>" !== globals["xyz"]) {
      console.log("values are different");
    }
  }
  globals["xyz"] = "<<XYZ>>";
?>

First we make sure it is defined and then we compare the stored value to the value of the current row. If the values are different then we log a message to the console.

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

DEPRECATED Use templatefx.first()

templatefx.group_end

DEPRECATED Use templatefx.last()

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.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.data[row][column]

This variable is a multi-dimensional array of the actual data with the headers in row "0". This allows you to access the raw data from whatever row you are currently processing.

templatefx.first (["field_1[ || field_2]"])

This variable is actually a JavaScript function and allows you to determine if this is the first time you have seen this particular field value. It will return true if this row of data is the first time this or these field values occur (the "field" variable is case insensitive). It also supports specifying multiple fields delimited by "||" if you wish to check if this is the first time you have seen this specific combination of values together. If you omit the "field" variable then it will return true for the first row of data. An example is included below which determines whether this is the first time we have seen this specific value within the "ROUTER" field:

<IF (templatefx.first("ROUTER"))>
! @ <<ROUTER>>
</IF>
DATA
templatefx.last (["field_1[ || field_2]"])

This variable is actually a JavaScript function and allows you to determine if this is the last time you will see this particular field value - the oppposite of templatefx.first(). It will return true if this row of data is the last time this or these field values occur (the "field" variable is case insensitive). It also supports specifying multiple fields delimited by "||" if you wish to check if this is the last time you have seen this specific combination of values together. If you omit the "field" variable then it will return true for the last row of data. An example is included below which determines whether this is the last time we have seen this specific combination of values for the "ROUTER" and "INTERFACE" fields:

<IF (templatefx.last("ROUTER||INTERFACE"))>
! ...
</IF>
DATA
templatefx.field ("field")

This function provides an alternative method of accessing the field values (the "field" variable is case insensitive) instead of using the <<FIELD>> syntax. 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}}>>). By using this function the following is now possible:

<FOR F = ["ROUTER", "INTERFACE", "STATE"]>
{{F}} has a value of <?= templatefx.field("{{F}}") ?> - equivalent of <<{{F}}>> which isn't supported.
</FOR>
templatefx.fields ("field", [{ "field_1": /regex/, "field_2": /regex/ }], [return_unique])

This function returns an array of all row values for a specific field - selected using the first parameter (all field names are treated as case-insensitive). There are a couple of optional arguments which allows the field values to be filtered as well as definining whether the returned array only contains unique values. At face value, this function looks quite complicated, but hopefully the following example will help explain:

NAME, AGE, COUNTRY
CHRIS, 65, GERMANY
PAUL, 32, FRANCE
EMMA, 32, ENGLAND
SIMON, 18, GERMANY
DORRIS, 32, ESTONIA

What follows are some examples using the above data - where an argument is passed as "null", it means use the default (i.e. don't filter and don't remove duplicate values).

# This will return an array of the following values: [CHRIS, PAUL, EMMA, SIMON, DORRIS]
#  Every "NAME" row value.
<?= templatefx.fields("NAME") ?>
          
# This will return an array of the following values: [GERMANY, FRANCE, ENGLAND, ESTONIA]
#  Every unique "COUNTRY" row value.
<?= templatefx.fields("COUNTRY", null, true) ?>         

# This will return an array of the following values: [CHRIS, SIMON]
#  Every "NAME" who has a "COUNTRY" that matches regex /^GERMANY$/.
<?= templatefx.fields("NAME", { "COUNTRY": /^GERMANY$/ }) ?>         

# This will return an array of the following values: [EMMA, DORRIS]
#  Every "NAME" who has a "COUNTRY" that matches regex /^e/i and an "AGE" that matches regex /^32$/.
<?= templatefx.fields("NAME", { "COUNTRY": /^e/i, "AGE": /^32$/ }) ?>

7.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.

base64encode ("string")

This function encodes a string using Base64 encoding. It will return the Base64 encoded string on success and null on failure.

base64decode ("base64 encoded string")

This function decodes a Base64 encoded string. It will return the decoded string on success and null on failure.

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 16 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
aggregate (new Array("cidr", "cidr", ...), [gaps]) IPv4 + IPv6

This function is used to find the shortest list of aggregate subnets that cover the list of IPv4/IPv6 subnets provided. It aggregates adjacent subnets ensuring the summary is explicit in only containing the provided subnets. By default it doesn't highlight where we have no contiguous blocks, but by setting the optional parameter "gaps" you can control it as follows:

gaps = 0 || null
This, or the default if omitted, will output a simple list of summarised prefixes without implying whether they are contiguous or discontiguous.

gaps = 1
This will output a list of summarised prefixes, but will include a "-" where there is a gap between the prefixes (i.e. if the list is discontiguous).

gaps = 2
This will output a list of summarised prefixes, but will fill in the missing prefixes to make the list contiguous from start to finish - any prefixes which fill the gaps will be prefixed with "-".

The following example demonstrates this function by outputting the aggregates one per row:

<?
  var subnets = [
    "192.0.2.0/31",
    "192.0.2.4 255.255.255.252",
    "192.0.2.8/30",
    "192.0.2.12/30",
    "192.0.2.16 255.255.255.248",
    "192.0.2.24/29",
    "2001:db8::8:800:200c:417a/64"
  ];
?>

<?= aggregate(subnets, 1).join("\n") ?>

This results in the following output:

192.0.2.0/31
-
192.0.2.4/30
192.0.2.8/29
192.0.2.16/28
-
2001:db8:0:0:0:0:0:0/64

If you were to set "gaps" to 2 then you would get the following with the gap filled in:

192.0.2.0/31
- 192.0.2.2/31
192.0.2.4/30
192.0.2.8/29
192.0.2.16/28
-
2001:db8:0:0:0:0:0:0/64
network ("cidr", [return_mask]) 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. By default it doesn't return the CIDR mask, but there is an optional parameter "return_mask", which if set to "true" will return it using CIDR notation.

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 (entries are separated using a semi-colon if multiple entries exist) 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>
curl ("url", ["basic_auth_string"], [ignore_ssl_cert], [timeout])

This function can be used to download http/https based content using the HTTP GET method. The only mandatory argument is "url" which is the same as what you would enter into your web browser (e.g. "https://www.google.com"). It supports HTTP Basic Authentication and if required can be invoked by providing a Base64 encoded string in the format of "username:password" as the "basic_auth_string" argument. HTTP Cookies are also supported and will persist for subsequent requests to the same server. If a HTTPS URL has been provided then it will check to ensure a valid trusted SSL certificate is being presented - you can override this behaviour by passing a value of "true" to the "ignore_ssl_cert" argument which will permit self-signed certificates to be accepted and bypass other security checks (this isn't advisable or recommended, but is provided out of necessity). The "timeout" argument can be used to specify a connection timeout in milliseconds - the default timeout is 5000 ms (5 seconds). At the moment this function only supports direct connections - the use of a proxy server isn't supported.

On success this function will return the contents of the HTTP response as a string and on failure it will return one of following numerical error codes:

0
An error occurred when trying to connect - the error message will be printed to the "Console" tab.

1
A timeout occurred when trying to connect - this might be temporary and if you try again it might succeed, if not then try increasing the "timeout" value.

100 to 599
The web server responded with a HTTP response code that wasn't 200.

<?
  var r = curl("http://templatefx.org");
  if (isNaN(r)) {
    // r = HTTP CONTENT
  }
  else if (r === 0) {
    // ERROR - SEE CONSOLE
  }
  else if (r === 1) {
    // TIMEOUT
  }
  else {
    // r = HTTP RESPONSE CODE
  }
?>

The above example demonstrates how you can check for success or failure when using this function. We use the isNaN() function to determine if we have a numerical or textual response - it assumes any numerical only response is some sort of error.

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") ?>
pad ("string", length, [align], [pchar])

This function provides left, right or centre alignment of text using padding. If the length of the "string" is less than the provided length, then the "string" will be padded. By default, the "string" is padded with space (or a different character via "pchar") on the right (left alignment), which can be changed using the optional third parameter with one of the following values:

align = 0 || null
This, or the default if omitted, will output the "string" with padding at the end of the text (left alignment).

align = 1
This will output the "string" with padding at the beginning of the text (right alignment).

align = 2
This will output the "string" with half the padding at the beginning and half at the end (centre alignment).

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. The "regex" is case-sensitive and must be provided as a string as opposed to the /regex/ JavaScript construct.

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.

banner ("text", ["pre_char"], ["fill_char"], ["charset"], ["post_char"])

This function generates an ASCII banner with the provided "text" - this mimics the Unix "banner" utility which is found in some distributions. It currently supports two character sets: "regular" and "small" with "regular" being the default which provides 7x7 upper case characters and 6x6 lower case characters. The "small" character set provides a subset (A-Z, 0-9 and -+.) of characters in a slightly smaller 5x5 form factor.

The "pre_char" argument is optional and allows you to specify a start of line character. In the same sense the "post_char" argument is also optional and allows you to specify an end of line character - both options can be specified as an empty string or null to use the default (nothing). The "fill_char" argument is also optional and will default to "#", but any character can be specified (even Unicode as per the example below). The "charset" argument allows you to switch between the two character sets. The following example demonstrates a banner using the text "TemplateFx" using the "FULL BLOCK (2588 in Hex)" UTF-8 character:

<?= banner("TemplateFx", null, "\u2588", "small") ?>

The above syntax will be translated into the following banner in your output:

█████  █████  █   █  ████   █       ███   █████  █████  █████  █   █
  █    █      ██ ██  █   █  █      █   █    █    █      █       █ █
  █    ███    █ █ █  ████   █      █████    █    ███    ███      █
  █    █      █   █  █      █      █   █    █    █      █       █ █
  █    █████  █   █  █      █████  █   █    █    █████  █      █   █
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.

7.3. External JavaScript Libraries

GUI Only Not supported within TemplateFx Server DataTemplates

In addition to the built-in JavaScript variables and functions, TemplateFx also supports globally including external JavaScript libraries. 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.

8. TemplateFx as a Service (REST API)

As it isn't always possible to deploy the Java Runtime Environment within a Desktop environment, maybe due to corporate policy or user preference, TemplateFx can also be deployed as a service instance using the TemplateFx Server component. In this scenario it provides a HTTP(S) based REST API to allow templating to occur from a number of different clients without having the burden of installing the TemplateFx GUI. Any client (i.e. Web Browser, Perl, PHP, Curl, Microsoft Excel, etc) which is able to establish a HTTP connection can use the REST API to generate outputs based on templates - it could be a service activation service that uses it to generate configuration on the fly from an order form, etc.

The TemplateFx Server is a cut down version of TemplateFx which is contained within a separate JAR file and only contains the core templating engine. It is run via the command line and supports the following command line options:

HTTP Server Mode

java -jar TemplateFxS.jar -s [host[:port]] [-dumpreq]

The "-s" parameter is used to initiate the TemplateFx Server and by default without any other parameters it will listen on TCP port 8088 on 127.0.0.1 only (localhost - doesn't accept remote connections). The "-dumpreq" parameter is used to output more verbose information on what it has been asked to process. By specifying a host of "0.0.0.0" or using a specific interface IP address it will accept remote connections from other hosts that have access to it. By default it will listen for standard HTTP connections (no TLS).

HTTPS Server Mode

java -jar TemplateFxS.jar -s [host[:port]] -tls <templatefx.jks> [-dumpreq]

As standard HTTP doesn't provide any security as the requests are sent in plaintext over the network (possibly acceptable when bound to localhost only), it also supports Transport Layer Security (TLS). By using the "-tls" parameter it allows you to specify a Java Key Store that contains your public/private key pair (where your public key is either self-signed or signed from a trusted Certificate Authority) - this enables TLS/HTTPS and disables HTTP so that all traffic between client and server is encrypted.

As an example using Java's keytool, the following demonstrates how to generate a self-signed RSA (or Elliptic Curve) key using a modulus of 4096 bits (or 384 for EC) with a validity of 1825 days (5 years):

keytool -genkeypair -keyalg RSA (or EC) -alias templatefx -keystore templatefx.jks -validity 1825 -keysize 4096 (or 384) -dname "CN=<CommonName>, C=<CountryName>, ST=<StateOrProvinceName>, L=<Locality>, O=<Organization>, emailAddress=<emailAddress>" -ext san=dns:<Host or IP> -v

By using self-signed certificates, the client will need to be told to trust this certificate as it hasn't been signed by a trusted Certificate Authority. The alternative and preferred way is to generate a Certificate Signing Request (CSR) and getting it signed by a CA that the client trusts. If you wish to go down the route of obtaining a signed certificate then you can generate a CSR from your key pair using the following:

keytool -certreq -alias templatefx -keyalg RSA (or EC) -file templatefx.csr -keystore templatefx.jks -ext san=dns:<Host or IP> -v

The contents of "templatefx.csr" can then be submitted to a public Certificate Authority to be signed, which can then be imported back (you will also need to import any Intermediary Certificates as well) into your Java Key Store:

keytool -import -alias templatefx -keystore templatefx.jks -file templatefx.cer

As a Java Key Store contains your private key it needs to be protected by a password - the TemplateFx Server can either prompt you for that password on startup or you can provide it via setting the environment variable "TFX_JKS_PW".

Offline Mode

java -jar TemplateFxS.jar -dt <dataTemplate> -out <outfile>

An alternative option is to use the TemplateFx Server component in Offline Mode by running it from the command line as and when you wish to generate an output. With this method, you provide it with an input DataTemplate and it will generate the output to the specified file. This could be called directly from a local script or from another program to generate outputs whenever you require, without having to support a TCP connection. The format of the DataTemplate is identical to the POST DATA associated with the REST API which is detailed below.

8.1. Request Format

To submit a request to the TemplateFx Server you need to use a standard HTTP POST request using the URL "/dataTemplate". The POST DATA should contain your DataTemplate in the text based format that is described in section 3 of this document - an example has been included below.

POST /dataTemplate HTTP/1.1
Host: 127.0.0.1:8088
Content-Type: text/plain; Charset=UTF-8
Content-Length: 483 

<templatefx:data>
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
</templatefx:data>

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

To support a more web friendly format, the "data" can also be submitted using JavaScript Object Notation (JSON) - the equivalent to the above is shown below.

<templatefx:data>
[
  { "ROUTER": "PE-1A", "INTERFACE": "Gi0/0/0", "IP_SUBNET": "192.0.2.0", "DEST": "R1 (Gi0/0)", "HELPERS": "192.0.2.129; 192.0.2.130; 192.0.2.131" },
  { "ROUTER": "PE-1A", "INTERFACE": "Gi0/0/1", "IP_SUBNET": "192.0.2.4", "DEST": "R2 (Gi0/1)", "HELPERS": "" },
  { "ROUTER": "PE-1A", "INTERFACE": "Gi0/0/2", "IP_SUBNET": "192.0.2.8", "DEST": "R3 (Gi0/2)", "HELPERS": "10.254.0.1" }
]
</templatefx:data>

8.2. Response Format

Assuming the request is valid HTTP and you don't get a HTTP error code back (i.e. something which isn't HTTP 200 OK), then there are two different response formats (all responses use JSON as per most REST APIs) - one for success and one for failure.

All responses will have a "version" field which is expected to be set to "1.0" as this defines the version of the API - any changes which radically change the response format will increase the version number. The "status" field is either "ok" or "error" depending on the response - when it is "ok" then it will return the generated outputs using array notation via the "outputs" object, which contains a separate hash containing a "group" and "value" key per output. In all instances the outputs themselves are encoded using Base64. The "console" field is also always included on success, but may be empty if nothing was written to it - again always Base64 encoded. When the status is "error" then it will return an error "code" and "message" to tell you what went wrong.

8.2.1. On Success

{
  "version": "1.0",
  "status": "ok",
  "elapsed": "<string>",
  "console": "<base64 string>",
  "outputs": [
    {
      "group": "<string>",
      "value": "<base64 string>"
    }
  ]
}

8.2.2. On Error

{
  "version": "1.0",
  "status": "error",
  "error": {
    "code": <integer>,
    "message": "<string>"
  }
}