Background

For some models of Ubiquiti Unifi router, you can use a config.gateway.json file to manipulate the full Vyatta config tree and make changes beyond what is offered in the UI.

To do this, you place a config.gateway.json file in a specific directory on the server (by default /var/lib/unifi/sites/<site_id>/config.gateway.json) and then trigger a “force provision” of the gateway.

Goals

Following infrastructure-as-code best practices, I use Ansible to manage my cloud-hosted Unifi servers, to the point where I never have to manually SSH to them or make any untracked changes.

Because each site on the Unifi server has its own directory and its own config.gateway.json file, I needed a way to easily track and version all the individual config.gateway.json files, keep track of what they do, keep track of where they need to go, roll them out to the correct directories, and reduce tedium and risk of error.

Implementation Overview

In Ansible, I created a new role and playbook to automatically prepare and roll out the config.gateway.json files for each site to the appropriate location on the Unifi server.

I store the desired configuration elements as native Ansible YAML, and I have Ansible convert them to JSON and put them in the right location.

Benefits

  • I don’t have to SSH to the Unifi controller to make config.gateway.json updates anymore, replacing this tedious step with proper Ansible.
  • I can name the configurations whatever I want in my Ansible, since Ansible knows how to map them to the appropriate site IDs.
  • I can write YAML instead of JSON and have it converted to JSON by Ansible, eliminating the risk of making a JSON formatting error and sending the Unifi gateway into a boot loop.
  • I can put inline comments in the YAML, which are stripped from the final JSON. JSON doesn’t support comments, so this improves the readability of the stored configuration vs. storing the JSON directly.
  • I no longer have a pile of config.gateway.json files lying around to individually track.

Implementation Details

First, I define Ansible variables with the configuration elements I want, and I put these in dedicated files in my Ansible source. Each one can go in its own file, since Ansible will pull variables from all .yaml files in a given group.

For example, at my customer1 site, I have a DNS mapping.

File: <ansible_root>/inventories/group_vars/unifi_01/config_customer1.yaml

unifi_config_customer1:
  system:
    static-host-mapping:
      host-name:
        # 1st floor printer, southwest corner
        myprinter.corp.mydomain.tld:
          inet:
            - "10.0.1.2"

At my customer2 site, I have an IPv6 tunnel.

File: <ansible_root>/inventories/group_vars/unifi_01/config_customer2.yaml

unifi_config_customer2:
  interfaces:
    tunnel:
      tun0:
        address:
          - "2001:db8::1/127"
        local-ip: "192.0.2.1"
        remote-ip: "203.0.113.1"
        encapsulation: "sit"
        description: "ipv6_tunnel"
        firewall:
          in:
            ipv6-name: "WANv6_IN"
          out:
            ipv6-name: "WANv6_OUT"
          local:
            ipv6-name: "WANv6_LOCAL"

Then I define another variable, so Ansible knows which variables to look for and where to put the results. This maps the friendly variable names in Ansible to the autogenerated site IDs for the sites in Unifi, so I don’t have to remember them myself.

File: <ansible_root>/inventories/group_vars/unifi_01/vars.yaml

unifi_site_config_map:
  unifi_config_customer1: "8q22y9b3"
  unifi_config_customer2: "g408ejm5"

With all that in place, it’s a simple series of two Ansible states to populate the appropriate contents into the appropriate config.gateway.json files.

File: <ansible_root>/roles/configure_unifi_config_gateway_json/tasks/main.yml

- name: "Create site directory if it does not already exist"
  file:
    path: "/var/lib/unifi/sites/{{ unifi_site_config_map[item]}}"
    state: "directory"
    owner: "unifi"
    group: "unifi"
    mode: "0750"
  with_items: "{{ unifi_site_config_map }}"

- name: "Write config.gateway.json file for all sites on controller"
  copy:
    content: "{{ lookup('vars', item) | to_nice_json(indent=2, sort_keys=False) }}\n"
    dest: "/var/lib/unifi/sites/{{ unifi_site_config_map[item]}}/config.gateway.json"
    owner: "unifi"
    group: "unifi"
    mode: "0640"
  with_items: "{{ unifi_site_config_map }}"

Running these tasks results in the proper JSON data in the proper location.

For customer1, in /var/lib/unifi/sites/8q22y9b3/config.gateway.json:

{
  "system": {
    "static-host-mapping": {
      "host-name": {
        "myprinter.corp.mydomain.tld": {
          "inet": [
            "10.0.1.2"
          ]
        }
      }
    }
  }
}

And for customer2, in /var/lib/unifi/sites/g408ejm5/config.gateway.json:

{
  "interfaces": {
    "tunnel": {
      "tun0": {
        "address": [
          "2001:db8::1/127"
        ],
        "local-ip": "192.0.2.1",
        "remote-ip": "203.0.113.1",
        "encapsulation": "sit",
        "description": "ipv6_tunnel",
        "firewall": {
          "in": {
            "ipv6-name": "WANv6_IN"
          },
          "out": {
            "ipv6-name": "WANv6_OUT"
          },
          "local": {
            "ipv6-name": "WANv6_LOCAL"
          }
        }
      }
    }
  }
}

Afterwards, all that’s left is to trigger a force provision on the target gateway from the Unifi controller.

Conclusion

This strategy helped me keep track of my growing pile of config.gateway.json files as I added more customers, it simplified a manual provisoining step, and it helped me ensure that as much of the system as possible is infrastructure-as-code.

I hope this is helpful for others who may be facing the same challenge!