Part of a series: Install · Configure · DHCP · Commission · Deploy · jq · SSH · More jq


The JSON output from MAAS CLI commands is both consistent and comprehensive, but hard for humans to process — especially at scale. Even one machine’s allocate or deploy output runs many pages. With 10 or 12 machines, it becomes unworkable.

Enter jq: a command-line tool for filtering and formatting JSON. With jq and a couple of Unix utilities, you can turn that wall of JSON into something like this:

HOSTNAME      SYSID   POWER  STATUS     OWNER  TAGS                 POOL     VLAN      FABRIC    SUBNET
--------      -----   -----  ------     -----  ----                 ----     ----      ------    ------
lxd-vm-1      r8d6yp  off    Deployed   admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-2      tfftrx  off    Allocated  admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-3      grwpwc  off    Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-4      6s8dt4  off    Deployed   admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-5      pyebgm  off    Allocated  admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-6      ebnww6  off    New        -      pod-console-logging  default  untagged  fabric-1
libvirt-vm-1  m7ffsg  off    Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
libvirt-vm-2  kpawad  off    Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
libvirt-vm-3  r44hr6  error  Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
libvirt-vm-4  s3sdkw  off    Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
libvirt-vm-5  48dg8m  off    Ready      -      pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
libvirt-vm-6  bacx77  on     Deployed   admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24

The command that produces this covers 99% of routine MAAS information needs. Let’s build it from scratch.


Watch it live

This material comes from a live Canonical show-and-tell session. If you’d rather watch than read:

MAAS Show and Tell: Using jq to make human-readable MAAS CLI output (Canonical MAAS Discourse, January 2021)


Basic jq usage

Pull hostnames from all machines, no formatting:

stormrider@wintermute:~$ maas admin machines read | jq '(.[] | [.hostname])'

Output:

[
  "lxd-vm-1"
]
[
  "lxd-vm-2"
]
...

Two things to note. First, jq instructions are enclosed in single quotes — they can span lines without line continuations:

maas admin machines read | jq '(.[]
| [.hostname])'

Second, .[] tells jq it’s decoding an array of datasets and should iterate through each one. The pipe (|) completes the “for each” construct. The outer brackets in the output represent each machine’s dataset boundary.

Adding a second field is just as simple:

stormrider@wintermute:~$ maas admin machines read | \
  jq '(.[] | [.hostname, .status_name])'

Output:

[
  "lxd-vm-1",
  "Deployed"
]
[
  "lxd-vm-2",
  "Allocated"
]
...

Improved formatting

Most Unix text-processing commands use tabs as field delimiters. The jq @tsv filter transforms output records into tab-separated lines:

maas admin machines read | jq '(.[] | [.hostname, .status_name]) | @tsv'

Output:

"lxd-vm-1\tDeployed"
"lxd-vm-2\tAllocated"
...

Close, but the quotes and \t representations aren’t human-readable. The -r (raw output) flag fixes that:

maas admin machines read | jq -r '(.[] | [.hostname, .status_name]) | @tsv'

Output:

lxd-vm-1    Deployed
lxd-vm-2    Allocated
...

Pipe to column -t to normalize column widths to the longest value in each column:

maas admin machines read | jq -r \
  '(.[] | [.hostname, .status_name]) | @tsv' | column -t

Adding headers

Pass a literal row to jq as the first expression:

maas admin machines read | jq -r \
  '(["HOSTNAME","STATUS"]),
   (.[] | [.hostname, .status_name]) | @tsv' | column -t

Output:

HOSTNAME      STATUS
lxd-vm-1      Deployed
lxd-vm-2      Allocated
...

Add a horizontal rule using map(length*"-"):

maas admin machines read | jq -r \
  '(["HOSTNAME","STATUS"] | (.,map(length*"-"))),
   (.[] | [.hostname, .status_name]) | @tsv' | column -t

Output:

HOSTNAME      STATUS
--------      ------
lxd-vm-1      Deployed
...

Handling null values

Only allocated or deployed machines have an owner. Unowned machines return null, which breaks column alignment. The jq alternate-value construct a // "b" means “if not a, use b”:

maas admin machines read | jq -r \
  '(["HOSTNAME","STATUS","OWNER","SYSTEM-ID"] | (.,map(length*"-"))),
   (.[] | [.hostname, .status_name, .owner // "-", .system_id])
   | @tsv' | column -t

Output:

HOSTNAME      STATUS     OWNER  SYSTEM-ID
--------      ------     -----  ---------
lxd-vm-1      Deployed   admin  r8d6yp
lxd-vm-3      Ready      -      grwpwc
...

Nested arrays

Tags are stored as a nested array:

"tag_names": [
    "pod-console-logging",
    "virtual"
],

Use tag_names[0] to get the first tag:

.tag_names[0] // "-"

Note: use underscores in column headers to prevent column -t from splitting on spaces — FIRST_TAG not FIRST TAG.


Nested keys

Some values aren’t top-level key-value pairs. The pool, for example:

"pool": {
    "name": "default",
    "description": "Default pool",
    "id": 0,
    ...
}

Asking for .pool directly causes an error. Use dot notation to reach the actual value:

.pool.name

Doubly-nested keys

VLAN and fabric names are nested inside boot_interface:

"boot_interface": {
    "vlan": {
        "name": "untagged",
        "fabric": "fabric-1",
        ...
    }
}

Access them with double indirection:

.boot_interface.vlan.name
.boot_interface.vlan.fabric

Deeply nested values

The subnet CIDR is buried inside boot_interface.links[]:

"boot_interface": {
    "links": [
        {
            "subnet": {
                "name": "10.124.141.0/24",
                ...
            }
        }
    ]
}

Access the first element of the links array:

.boot_interface.links[0].subnet.name

The complete table command

Putting it all together:

maas admin machines read | jq -r '(["HOSTNAME","SYSID","POWER","STATUS",
"OWNER", "TAGS", "POOL", "VLAN","FABRIC","SUBNET"] | (., map(length*"-"))),
(.[] | [.hostname, .system_id, .power_state, .status_name, .owner // "-",
.tag_names[0] // "-", .pool.name,
.boot_interface.vlan.name, .boot_interface.vlan.fabric,
.boot_interface.links[0].subnet.name]) | @tsv' | column -t

Sorting with awk

To sort by hostname while keeping headers pinned at the top, use awk to sort only rows after the first two:

maas admin machines read | jq -r '(["HOSTNAME","SYSID","POWER","STATUS","OWNER",
"TAGS", "POOL", "VLAN","FABRIC","SUBNET"] | (., map(length*"-"))),(.[] |
[.hostname, .system_id, .power_state, .status_name, .owner // "-",
.tag_names[0] // "-", .pool.name, .boot_interface.vlan.name,
.boot_interface.vlan.fabric,.boot_interface.links[0].subnet.name])
| @tsv' | column -t | awk 'NR<3{print $0;next}{print $0| "sort -k 1"}'

Change -k 1 to -k 4 to sort by status, -k 5 for owner, and so on.


jq is a simple but powerful tool for working with MAAS CLI output. And since it outputs plain text, anything you can do with text output in Unix, you can do with jq results. Next up: SSH and SCP with deployed machines.