Skip to content

JTAF Basic Rules

David Gee edited this page Nov 26, 2021 · 7 revisions

There are some rules to JTAF which must be adhered to. There are edge cases to these rules and please make sure you read them before filing bugs. If you break the rules, you could waste significant time debugging non-issues.

1. In .tf files, each resource must concern itself with the smallest amount of work absolutely possible.

Good examples are setting an accept attribute on a route filter, or a instance type in a firewall security rule. It might even be to create the cliché vlan. Terraform providers generated by JTAF have simple logic for the CRUD (Create, Read, Update, Delete) internal Terraform operations. To explain further, Terraform resources are stored in Junos configuration groups, which are simple to create, read and delete, but when it comes to updating, the most simple logic is to destroy the entire group and rebuild it (without committing). If multiple resources have references to one Junos configuration group and that config group is blown away as part of an update, Terraform's local state will be incomplete when compared to the remote.

Why groups you ask? Glad you asked. The Junos configuration will inherit each group and merge the contents of the contents at commit time. Junos configuration groups are effectively named config blobs that can be managed through the Junos NETCONF transaction engine. However, we do not want to burden our provider code with having to understand the Junos configuration and to keep the project simple and the code maintainable, we concentrate on the smallest blobs of concern.

Here is an example of two different groups. Junos will converge both of these entries into the global address book.

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_demo_1" {
    resource_name = "vsrx_demo_1"
    name = "global"
    name__1 = "partner_net_1"
    ip__prefix = "10.0.0.0/24"
}

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_demo_2" {
    resource_name = "vsrx_demo_2"
    name = "global"
    name__1 = "partner_net_2"
    ip__prefix = "10.0.1.0/24"
}

They appear in Junos like this:

vsrx_demo_1 {
    security {
        address-book {
            global {
                address partner_net_1 10.0.0.0/24;
            }
        }
    }
}
vsrx_demo_2 {
    security {
        address-book {
            global {
                address partner_net_2 10.0.1.0/24;
            }
        }
    }
}

Here is the same style represented in a single group.

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_demo_3" {
    resource_name = "vsrx_demo_3"
    name = "global"
    name__1 = "partner_net_3"
    ip__prefix = "10.0.0.0/24"
}

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_demo_4" {
    resource_name = "vsrx_demo_3"
    name = "global"
    name__1 = "partner_net_4"
    ip__prefix = "10.0.1.0/24"
}

Here is the resulting Junos configuration:

vsrx_demo_3 {
    security {
        address-book {
            global {
                address partner_net_3 10.0.0.0/24;
                address partner_net_4 10.0.1.0/24;
            }
        }
    }
}

Here, both entries are contained in the same Junos group and we've broken our smallest concern rule. Therefore, if we update the code in HCL for partner_net_2 and run terraform to make the appropriate changes, partner_net_1 will vanish. This is an expected outcome of the way the driver handles update transactions and is not a bug. Secondly, if we run a terraform plan after the previous apply, then Terraform informs us of a change that's required:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # junos-device_SecurityAddress__BookAddressIp__Prefix.vsrx_demo_3 will be updated in-place
  ~ resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_demo_3" {
        id            = "127.0.0.1_vsrx_demo_3"
      ~ ip__prefix    = "10.0.1.0/24" -> "10.0.0.0/24"
        name          = "global"
      ~ name__1       = "partner_net_4" -> "partner_net_3"
        resource_name = "vsrx_demo_3"
    }

Plan: 0 to add, 1 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

It does this because we're overwriting the local state when the remote state data is fetched. This is the danger of using things like a list of resources in a single resource. Everything in JTAF is designed around the notion of smallest unit of concern and despite HCL being able to support different data types like sets and lists etc, it's now how JTAF has been designed.

2. Avoid inserting whitespace into key values within HCL code.

If you have a resource like below, the name__1 key will get rejected by Junos and the transaction will fail. This error is a Junos NETCONF constraint which leaks through to Terraform through providers generated by JTAF.

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_1" {
    resource_name = "vsrx_1"
    name = "global"
    name__1 = " partner_net_1"
    ip__prefix = "10.0.0.0/24"
}

3. YANG choice-ident semantics are supported, but...

In YANG models, there are intra and inter-model dependencies. The keyword uses within a leaf or list is supported for choice-ident keyed fields. This key name relies on enumerated fields, which are in essence empty XML elements that are passed through to Junos. In order to activate the element within providers generated by JTAF, you must provide a single whitespace to the value for the key. This might seem crazy, but it's harmonious to both Junos and Terraform's HCL implementation. If you pass in any other value but a whitespace, then Junos will fail the RPC and the transaction will fail.

Here's an example of that with a route filter resource. The orlonger element will be passed in with the address field. In essence, as you figure out the 'right-sized' config blob that Junos would require, you can convert that to a Terraform resource.

The CLI would be set policy-options policy-statement term t1 from route-filter 10.0.0.0/24 orlonger

Hopefully it's clear how we get from CLI config to HCL config (below).

resource "junos-device_Policy__OptionsPolicy__StatementTermFromRoute__FilterAddress" "vsrx_1" {
    resource_name = "vsrx_1"
    name = "DEF-IMPORT-FWTRUST-TABLE"
    name__1 = "t1"
    address = "10.0.0.0/24"
    orlonger = " "
}

4. YANG choice elements. Not the same as choice-ident

You must include the entire XPath expression for the choice element and include it with "" within HCL like below.

resource "junos-device_Policy__OptionsPolicy__StatementTermThenAccept" "vsrx_1" {
    resource_name = "vsrx_1"
    name = "DEF-IMPORT-FWTRUST-TABLE"
    name__1 = "t1"
    accept = ""
}

5. Commit Sequences

We get to the crown jewel of Junos - being commit based transactions. When we issue terraform apply operations to our network elements, Terraform instantiates an in memory graph, so it knows what resources to deploy and in what order. Our design guidance is to use Terraform modules, which is a deployment pattern for structuring .tf files. Our goal is to create a tree of .tf resources, with a single commit resource depending on a module of the actual resources to be deployed. This means in the Terraform graph, the resources are created first and the commit is issued last.

When running terraform destroy the last resource to be destroyed will be a commit resource, which in essence is another commit sequence. This logic allows us to commit on terraform apply and terraform destroy. Hopefully that makes sense.

Here is an example of a Terraform Module in the samples directory.

6. Ordering

Where resources are required in a specific order, or with resources that have significant dependencies, you can use the depends_on = [] key within a resource, pointing to one or more Terraform resource addresses. With independent groups and the smallest unit of concern approach we take, this is a non-issue, but there may be edge cases we're not currently aware of.

Here's an example of depends_on. It doesn't make sense semantically, but it shows the syntax. A better example would be an irb relying on a vlan to be present. This is great for ensuring the conditions are present for a successful deployment and can provide guard rails for botched deployments.

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_1" {
    resource_name = "vsrx_1"
    name = "global"
    name__1 = "partner_net_1"
    ip__prefix = "10.0.0.0/24"
}

resource "junos-device_SecurityAddress__BookAddressIp__Prefix" "vsrx_2" {
    resource_name = "vsrx_2"
    name = "global"
    name__1 = "partner_net_2"
    ip__prefix = "10.0.1.0/24"
    depends_on = [junos-device_SecurityAddress__BookAddressIp__Prefix.vsrx_1]
}

7. Updating Resources and Deleting Individual Ones

Junos relies on the commit actions to be applied at the end of each transaction. Terraform doesn't know anything about transactions, so we must consider how to apply commits. If you look in the directory samples/tf_module_template, there is the best practice module way of using JTAF providers. A commit is applied last to a bunch of resources when terraform apply is ran. On terraform-destroy there is another commit which uses inverse logic (i.e., the commit is applied when the delete action is called. This is in source code, so don't worry too much!).

If you're using the Terraform module as a template, then there's one situation you will find yourself in that you need to address. That is in the case of deleting individual resources or updating them.

You will need to taint the commit resource so that Terraform will apply a commit after the terraform apply stage. A taint does nothing more than mark the resource for a re-deploy. The input to the command is a Terraform address, which references a resource in the main.tf top level module file.

terraform taint junos-vsrx_commit.commit-main

When you run terraform apply after the taint and making other changes to one or more resources, the commit will be applied.

Clone this wiki locally