Unifi config.gateway.json management with Ansible
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!