Infoblox, SRX Dynamic Group Updates Using Go

I was having some discussions a while back regarding automation with some of my fellow Juniper Ambassadors, and one of the topics that came up was how cool it would be to be have the ability for an SRX to dynamically update address groups based on, for example, changes to hosts/networks within IPAM (Infoblox).

If you haven't heard of Infoblox, it is an enterprise DNS/DHCP solution...and one of the best if not the best, IMO.

I have been hacking away at it lately, and I have a working solution that I'll show you. I'm using all standard/builtin Go libraries, and the go-junos library for my interaction with the SRX and Junos Space.

What does the script do?

Basically, it does the following:

  • Query Infoblox based on the given search term, and return all host addresses that match.
    • Currently this example only searches for hosts, but I'm going to work at searching for networks as well.
    • You can also search fields other than the "comments" one that I am using in this example (e.g. "subnet_org").
  • Once we have all the addresses, we generate the address-book/address-set configuration in set format.
  • Connect to the given SRX, and commit the configuration
  • Displays our changes from the previous configuration (compares to rollback 1).

The Junos Space example does the exact same things, except that we create the address objects and group as objects in Space, then publish the policy which references the address group.

When we query Infoblox, I am choosing to return the results in XML format (default is JSON). Here's a sample of what it looks like:

<list>
    <value type="object">
        <_ref>record:host/ZG5zLmhvc3QkLl9kZWZhdWx0LmNvbS5tZWlqZXIuYmxnOTc1LncwOTc1ZXVzckwMTAw:sdubs-fw/Internal</_ref>
        <view>Internal</view>
        <ipv4addrs>
            <list>
                <value type="object">
                    <configure_for_dhcp type="boolean">false</configure_for_dhcp>
                    <host>sdubs-fw</host>
                    <ipv4addr>1.1.1.1</ipv4addr>
                    <_ref>record:host_ipv4addr/ZG5zLmhvc3RfYWRkcmVzcyQuX2RlZmF1bHQuY29tLm1laWplci5ibGc5NzUudzA5NzVldXNyaTAxMDAuMTAuMTcuMi4xOS4:1.1.1.1/sdubs-fw/Internal</_ref>
                </value>
            </list>
        </ipv4addrs>
        <name>sdubs-fw</name>
    </value>
    <value type="object">
        <_ref>record:host/ZG5zLmhvc3QkLl9kZWZhdWx0LmNvbS5tZWlqZXIuYmxnOTc1LncwOTc1ZXVzckwMTAw:sdubs-ap/Internal</_ref>
        <view>Internal</view>
        <ipv4addrs>
            <list>
                <value type="object">
                    <configure_for_dhcp type="boolean">false</configure_for_dhcp>
                    <host>sdubs-ap</host>
                    <ipv4addr>1.1.1.2</ipv4addr>
                    <_ref>record:host_ipv4addr/ZG5zLmhvc3RfYWRkcmVzcyQuX2RlZmF1bHQuY29tLm1laWplci5ibGc5NzUudzA5NzVldXNyaTAxMDAuMTAuMTcuMi4xOS4:1.1.1.1/sdubs-ap/Internal</_ref>
                </value>
            </list>
        </ipv4addrs>
        <name>sdubs-ap</name>
    </value>
</list>

And after we commit our configuration to the SRX, here's what the config diff looks like:

[edit security address-book global]
     address ware-lan { ... }
+    address sdubs-fw 1.1.1.1/32;
+    address sdubs-ap 1.1.1.2/32;
[edit security address-book global]
+    address-set Dynamic-IPAM-Hosts {
+        address sdubs-fw;
+        address sdubs-ap;
+    }

And with that...here's the full code for the SRX script. Also available as a Github Gist. Here is the Gist for the Junos Space example.

For this example, I made a binary and ran it, but you can just do a go run <script> if you'd like.

package main

import (
	"encoding/xml"
	"fmt"

	"github.com/scottdware/go-junos"
	"github.com/scottdware/go-rested"
)

// dynamicHosts parses the overall XML returned from the Infoblox query.
type dynamicHosts struct {
	XMLName xml.Name    `xml:"list"`
	Hosts   []hostEntry `xml:"value>ipv4addrs>list>value"`
}

// hostEntry parses the XML for each individual host.
type hostEntry struct {
	Name    string `xml:"host"`
	Address string `xml:"ipv4addr"`
}

var (
	searchString = "ware"
	ipamGM       = "infoblox.company.com"
	ipamUser     = "ibadmin"
	ipamPass     = "infoblox"
	srxHost      = "srx240.company.com"
	srxUser      = "juniper"
	srxPass      = "Juniper123!"
	groupName    = "Dynamic-IPAM-Hosts"
)

// queryIPAM searches throughout Infoblox for the given search string...which matches the "comment" field
// within Infoblox and returns addresses that we will build our address-set for.
func queryIPAM() (*dynamicHosts, error) {
	var data dynamicHosts
	reqURL := fmt.Sprintf("https://%s/wapi/v1.0/record:host?comment~:=%s", ipamGM, searchString)
	r := rested.NewRequest()
	r.BasicAuth(ipamUser, ipamPass)
	headers := map[string]string{
		"Accept": "application/xml",
	}

	// Send our HTTP request to Infoblox.
	resp := r.Send("get", reqURL, nil, headers, nil)
	if resp.Error != nil {
		fmt.Println(resp.Error)
	}

	err := xml.Unmarshal(resp.Body, &data)
	if err != nil {
		return nil, err
	}

	return &data, nil
}

func main() {
	var srxConfig []string

	// Run our query against IPAM/Infoblox to get our addresses.
	d, err := queryIPAM()
	if err != nil {
		fmt.Println(err)
	}

	// Create our address entries first, and append them to our config.
	for _, ae := range d.Hosts {
		srxConfig = append(srxConfig, fmt.Sprintf("set security address-book global address %s %s/32\n", ae.Name, ae.Address))
	}

	// Create the address-set/group and assign the addresses we just created to it.
	for _, as := range d.Hosts {
		srxConfig = append(srxConfig, fmt.Sprintf("set security address-book global address-set %s address %s\n", groupName, as.Name))
	}

	// Connect to our SRX.
	jnpr, err := junos.NewSession(srxHost, srxUser, srxPass)
	if err != nil {
		fmt.Println(err)
	}

	// Load our configuration into the SRX from the "srxConfig" variable we set earlier.
	err = jnpr.Config(srxConfig, "set", false)
	if err != nil {
		fmt.Println(err)
	}

	// Commit the configuration to our SRX.
	jnpr.Commit()

	// Print the changes out to the console.
	changes, _ := jnpr.ConfigDiff(1)
	fmt.Println(changes)
}

Here is the Junos Space example code:

package main

import (
	"encoding/xml"
	"fmt"

	"github.com/scottdware/go-junos"
	"github.com/scottdware/go-rested"
)

// dynamicHosts parses the overall XML returned from the Infoblox query.
type dynamicHosts struct {
	XMLName xml.Name    `xml:"list"`
	Hosts   []hostEntry `xml:"value>ipv4addrs>list>value"`
}

// hostEntry parses the XML for each individual host.
type hostEntry struct {
	Name    string `xml:"host"`
	Address string `xml:"ipv4addr"`
}

var (
	searchString = "ware"
	ipamGM       = "infoblox.company.com"
	ipamUser     = "ibadmin"
	ipamPass     = "infoblox"
	spaceHost    = "junosspace.company.com"
	spaceUser    = "juniper"
	spacePass    = "Juniper123!"
	groupName    = "Dynamic-IPAM-Hosts"
)

// queryIPAM searches throughout Infoblox for the given search string...which matches the "comment" field
// within Infoblox and returns addresses that we will build our address-set for.
func queryIPAM() (*dynamicHosts, error) {
	var data dynamicHosts
	reqURL := fmt.Sprintf("https://%s/wapi/v1.0/record:host?comment~:=%s", ipamGM, searchString)
	r := rested.NewRequest()
	r.BasicAuth(ipamUser, ipamPass)
	headers := map[string]string{
		"Accept": "application/xml",
	}

	// Send our HTTP request to Infoblox.
	resp := r.Send("get", reqURL, nil, headers, nil)
	if resp.Error != nil {
		fmt.Println(resp.Error)
	}

	err := xml.Unmarshal(resp.Body, &data)
	if err != nil {
		return nil, err
	}

	return &data, nil
}

func main() {
	// Connect to Junos Space.
	jspace := junos.NewServer(spaceHost, spaceUser, spacePass)

	// Run our query against IPAM/Infoblox to get our addresses.
	d, err := queryIPAM()
	if err != nil {
		fmt.Println(err)
	}

	// Create our address entries/objects in Space.
	for _, ae := range d.Hosts {
		jspace.AddAddress(ae.Name, ae.Address)
	}

	// Create the address group and assign the addresses we just created to it.
	jspace.AddGroup("address", groupName, "IP addresses from Infoblox")

	for _, as := range d.Hosts {
		jspace.EditGroup("address", "add", as.Name, groupName)
	}

	// Let's assume we have a policy named "Fireall Policy" that references the address-group we created above.
	// Now we can push the policy out and update the associated SRX's.
	jobID, err := jspace.PublishPolicy("Firewall Policy", true)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("Job ID: %d\n", jobID)
}

This script is run manually now, but you could easily create a scheduled job, or maybe with some more coding, check for changes every so often and then run a script like this to publish the updates hosts/networks to your SRX.