Improving CLI output with jq

Bill Wear

on 6 January 2021

This article was last updated 3 years ago.


Welcome back to our series on MAAS CLI operations. In our previous post, we learned how to acquire and deploy machines using the MAAS CLI. It was also evident that the JSON output from the allocate and deploy commands was very lengthy for even one machine — so you can imagine how large a list of 10 or 12 machines might be. Traditional JSON output is both consistent and comprehensive, but sometimes hard for humans to process.

Enter jq, a command-line tool dedicated to filtering and formatting JSON output, so that you can more easily summarize data. For instance, consider a small MAAS install with 12 virtual machines. Six of these machines are lxd VMs, and six are libvirt VMs. If we were to enter the MAAS CLI command to list all those machines:

maas admin machines read

the listing would be many pages long, and likely very time-consuming to pick through. On the other hand, with the jq command, a couple of other Ubuntu CLI commands, and just a little bit of finesse:

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

we can produce an useful and compact machine listing that serves 99% of the routine information needs of most users:

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

Here we have a clean text table listing the machine hostnames, along with the system IDs, power states, machines statuses, tags, pools, and networking information. These parameters represent only a small fraction of the available JSON output, of course. Let’s break this command down, piece by piece, and see how it works.

Basic jq usage

First, we’ll just pull the hostnames from these machines, with no qualifiers or formatting rules, like this:

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

This command returns output that looks something like this:

 [
"lxd-vm-1"
]
[
"lxd-vm-2"
]
[
"lxd-vm-3"
]
[
"lxd-vm-4"
]
[
"lxd-vm-5"
]
[
"lxd-vm-6"
]
[
"libvirt-vm-1"
]
[
"libvirt-vm-2"
]
[
"libvirt-vm-3"
]
[
"libvirt-vm-4"
]
[
"libvirt-vm-5"
]
[
"libvirt-vm-6"
]

Note a couple of things about this command:

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

First, the jq instructions are enclosed in single quotes. As such, they can span lines if necessary, without any line continuations (\), like this:

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

Second, notice the structure of the jq instructions. The .[] tells jq that it’s decoding an array of data sets — in this case, an array of machine data sets — and that it should iterate through each of the outer data sets (each machine) individually. The pipe symbol (|) completes the “for each” construct, so this command basically says, “for each set of machine data you get, pull out (and return) the value associated with the JSON key hostname. The return value reflects this structure:

[
"libvirt-vm-5"
]
[
"libvirt-vm-6"
]

The outer square brackets represent the boundaries of each machine’s data set, and the value in quotes corresponds to the value of the key hostname in successive machine data sets. It can get a little complicated sometimes, but that’s basically the way to parse JSON with jq.

For practice let’s try pulling the value of the key that holds machine status, again with no qualifiers or special formatting:

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

This command essentially tells jq to do the same thing as last time, but also collect the value of the key “status_name” for each machine. The results looks something like this:

[
  "lxd-vm-1",
  "Deployed"
]
[
  "lxd-vm-2",
  "Allocated"
]
[
  "lxd-vm-3",
  "Ready"
]
[
  "lxd-vm-4",
  "Deployed"
]
[
  "lxd-vm-5",
  "Allocated"
]
[
  "lxd-vm-6",
  "New"
]
[
  "libvirt-vm-1",
  "Ready"
]
[
  "libvirt-vm-2",
  "Ready"
]
[
  "libvirt-vm-3",
  "Ready"
]
[
  "libvirt-vm-4",
  "Ready"
]
[
  "libvirt-vm-5",
  "Ready"
]
[
  "libvirt-vm-6",
  "Deployed"
]

So much for printing the values of JSON keys. There are still some nuances (arrays, nested keys, …), but this is the lion’s share of the syntax. Let’s divert for a minute and look at how to format the output in a more human-readable way.

Improved formatting

Most of the Ubuntu text-processing commands use tabs as field delimiters, which is a trait inherited from grandfather UNIX. Currently, the output is clean, but relatively hard to format into lines. Luckily jq has a filter for this: the “tab-separated values” filter, known as @tsv. This filter transforms the output records into individual lines with values separated by tabs.

Adding @tsv to the mix:

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

we get something like this:

"lxd-vm-1\tDeployed"
"lxd-vm-2\tAllocated"
"lxd-vm-3\tReady"
"lxd-vm-4\tDeployed"
"lxd-vm-5\tAllocated"
"lxd-vm-6\tNew"
"libvirt-vm-1\tReady"
"libvirt-vm-2\tReady"
"libvirt-vm-3\tReady"
"libvirt-vm-4\tReady"
"libvirt-vm-5\tReady"
"libvirt-vm-6\tDeployed"

That’s a step in the right direction, but it’s still pretty far from human-readable output. If only there were some way to get rid of the quotes and just do the tab, instead of representing it as a regex character. In fact, the jq “raw” output option (-r) takes care of this:

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

Feeding the raw output into our three-filter set gives us a more readable result:

lxd-vm-1	Deployed
lxd-vm-2	Allocated
lxd-vm-3	Ready
lxd-vm-4	Deployed
lxd-vm-5	Allocated
lxd-vm-6	New
libvirt-vm-1	Ready
libvirt-vm-2	Ready
libvirt-vm-3	Ready
libvirt-vm-4	Ready
libvirt-vm-5	Ready
libvirt-vm-6	Deployed

This is tabulated, but the number of spaces between the columns is a little big, and, if there’s an unusually long value in one of the fields, it may throw the tabulation off for that line. Something could have been added to jq for that, but there is no need, since Ubuntu already has the column utility. Piping the output of the command so far to column -t (-t for “tabs”) will normalize the tab spacing to the data and ensure that each column is exactly long enough for the longest value in that column:

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

This command result is very similar to the previous output, though you’ll notice that the field spacing is neatly optimized to the data itself:

lxd-vm-1      Deployed
lxd-vm-2      Allocated
lxd-vm-3      Ready
lxd-vm-4      Deployed
lxd-vm-5      Allocated
lxd-vm-6      New
libvirt-vm-1  Ready
libvirt-vm-2  Ready
libvirt-vm-3  Ready
libvirt-vm-4  Ready
libvirt-vm-5  Ready
libvirt-vm-6  Deployed

Making real tables

So far, so good, but this still isn’t a presentable data table. First of all, there are no headings. These can be added by passing a literal row to jq, like this:

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

You’ll note that there are two expressions in parenthesis (representing individual lines or rows). The first just contains the two column headings, while the second contains the “for each” construct that pulls the hostname and status out of the JSON. In essence, the first expression evaluates to just one row, since there’s nothing to tell it to iterate. The second expression evaluates to one row per machine, since that’s the level of data we’re reading. Here’s what we get from this command:

HOSTNAME      STATUS
lxd-vm-1      Deployed
lxd-vm-2      Allocated
lxd-vm-3      Ready
lxd-vm-4      Deployed
lxd-vm-5      Allocated
lxd-vm-6      New
libvirt-vm-1  Ready
libvirt-vm-2  Ready
libvirt-vm-3  Ready
libvirt-vm-4  Ready
libvirt-vm-5  Ready
libvirt-vm-6  Deployed

Nice, but it needs a horizontal rule, like a line of dashes, to separate the headings from the data. We can do this by essentially turning the one header row into two, using some jq macros to generate dashes lines of appropriate length:

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

The expression | (.,) tells jq to convert the foregoing header row into two rows: the first contains the two headers, as in the previous row, and the second contains the result of a couple of macros (map and length). We won’t detail those here, but the use of this construct produces the following output:

HOSTNAME      STATUS
--------      ------
lxd-vm-1      Deployed
lxd-vm-2      Allocated
lxd-vm-3      Ready
lxd-vm-4      Deployed
lxd-vm-5      Allocated
lxd-vm-6      New
libvirt-vm-1  Ready
libvirt-vm-2  Ready
libvirt-vm-3  Ready
libvirt-vm-4  Ready
libvirt-vm-5  Ready
libvirt-vm-6  Deployed

Extending the list

Let’s add a couple more fields, owner (which is sometimes blank), and system_id (which is never blank), to the output:

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

This gives us the following result:

HOSTNAME      STATUS     OWNER   SYSTEM-ID
--------      ------     -----   ---------
lxd-vm-1      Deployed   admin   r8d6yp
lxd-vm-2      Allocated  admin   tfftrx
lxd-vm-3      Ready      grwpwc  
lxd-vm-4      Deployed   admin   6s8dt4
lxd-vm-5      Allocated  admin   pyebgm
lxd-vm-6      New        ebnww6  
libvirt-vm-1  Ready      m7ffsg  
libvirt-vm-2  Ready      kpawad  
libvirt-vm-3  Ready      r44hr6  
libvirt-vm-4  Ready      s3sdkw  
libvirt-vm-5  Ready      48dg8m  
libvirt-vm-6  Deployed   admin   bacx77

You’ll notice right away there’s a problem with the columns. Remember that only machines in the “Allocated” or “Deployed” state are owned by anyone, since that’s what allocate/acquire means. The lines for the deployed and allocated machines lay out correctly, but the lines for the unowned machines are incorrectly formatted. We can fix this by using the jq “alternate value” construct (a // "b"), which can be loosely read, “if not a, then b.” We add it to the owner key like this:

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

Then the results line up nicely, based on the longest value in each key column:

HOSTNAME      STATUS     OWNER  SYSTEM-ID
--------      ------     -----  ---------
lxd-vm-1      Deployed   admin  r8d6yp
lxd-vm-2      Allocated  admin  tfftrx
lxd-vm-3      Ready      -      grwpwc
lxd-vm-4      Deployed   admin  6s8dt4
lxd-vm-5      Allocated  admin  pyebgm
lxd-vm-6      New        -      ebnww6
libvirt-vm-1  Ready      -      m7ffsg
libvirt-vm-2  Ready      -      kpawad
libvirt-vm-3  Ready      -      r44hr6
libvirt-vm-4  Ready      -      s3sdkw
libvirt-vm-5  Ready      -      48dg8m
libvirt-vm-6  Deployed   admin  bacx77

Nested arrays

Machines have a nested array (of indeterminate length) for machine tags. In JSON terms, instead of having a single key-value pair at the top level, like this:

"hostname": "libvirt-vm-6",

tags are represented by nested arrays, like this:

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

Incorporating a random number of tags per machine into a neat table is beyond the scope of this particular post, but we can show the first tag in the table rows:

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

Where we would use .json-key-name for a non-nested value, we need only use .json-key-name[0] to refer to the first element of the nested array. Doing this produces the following result:

HOSTNAME      STATUS     OWNER  SYSTEM-ID  FIRST                TAG
--------      ------     -----  ---------  ---------            
lxd-vm-1      Deployed   admin  r8d6yp     pod-console-logging  
lxd-vm-2      Allocated  admin  tfftrx     pod-console-logging  
lxd-vm-3      Ready      -      grwpwc     pod-console-logging  
lxd-vm-4      Deployed   admin  6s8dt4     pod-console-logging  
lxd-vm-5      Allocated  admin  pyebgm     pod-console-logging  
lxd-vm-6      New        -      ebnww6     pod-console-logging  
libvirt-vm-1  Ready      -      m7ffsg     pod-console-logging  
libvirt-vm-2  Ready      -      kpawad     pod-console-logging  
libvirt-vm-3  Ready      -      r44hr6     pod-console-logging  
libvirt-vm-4  Ready      -      s3sdkw     pod-console-logging  
libvirt-vm-5  Ready      -      48dg8m     pod-console-logging  
libvirt-vm-6  Deployed   admin  bacx77     pod-console-logging  

That’s almost right, but notice that the heading separates on spaces between words. Let’s try a better way, with an underscore:

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

This version of the command produces the expected output:

HOSTNAME      STATUS     OWNER  SYSTEM-ID  FIRST_TAG
--------      ------     -----  ---------  ---------
lxd-vm-1      Deployed   admin  r8d6yp     pod-console-logging
lxd-vm-2      Allocated  admin  tfftrx     pod-console-logging
lxd-vm-3      Ready      -      grwpwc     pod-console-logging
lxd-vm-4      Deployed   admin  6s8dt4     pod-console-logging
lxd-vm-5      Allocated  admin  pyebgm     pod-console-logging
lxd-vm-6      New        -      ebnww6     pod-console-logging
libvirt-vm-1  Ready      -      m7ffsg     pod-console-logging
libvirt-vm-2  Ready      -      kpawad     pod-console-logging
libvirt-vm-3  Ready      -      r44hr6     pod-console-logging
libvirt-vm-4  Ready      -      s3sdkw     pod-console-logging
libvirt-vm-5  Ready      -      48dg8m     pod-console-logging
libvirt-vm-6  Deployed   admin  bacx77     pod-console-logging

Nested keys

These aren’t all the routine key-value pairs we want in the table, though. It would also be nice to print the pool to which each machine is assigned. Just asking for .pool as a single key-value pair:

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

produces an error:

jq: error (at <stdin>:5639): object ({"name":"de...) is not valid in a csv row

Looking at the JSON output, we see that .pool is a nested key, not a key-value pair:

        "pool": {
            "name": "default",
            "description": "Default pool",
            "id": 0,
            "resource_uri": "/MAAS/api/2.0/resourcepool/0/"
        },

What we really want is the pool name, so we need to add one level of indirection to that particular key to reach the actual key-value pair, like this:

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

which gives us what we want:

HOSTNAME      STATUS     OWNER  SYSTEM-ID  FIRST_TAG            POOL
--------      ------     -----  ---------  ---------            ----
lxd-vm-1      Deployed   admin  r8d6yp     pod-console-logging  default
lxd-vm-2      Allocated  admin  tfftrx     pod-console-logging  default
lxd-vm-3      Ready      -      grwpwc     pod-console-logging  default
lxd-vm-4      Deployed   admin  6s8dt4     pod-console-logging  default
lxd-vm-5      Allocated  admin  pyebgm     pod-console-logging  default
lxd-vm-6      New        -      ebnww6     pod-console-logging  default
libvirt-vm-1  Ready      -      m7ffsg     pod-console-logging  default
libvirt-vm-2  Ready      -      kpawad     pod-console-logging  default
libvirt-vm-3  Ready      -      r44hr6     pod-console-logging  default
libvirt-vm-4  Ready      -      s3sdkw     pod-console-logging  default
libvirt-vm-5  Ready      -      48dg8m     pod-console-logging  default
libvirt-vm-6  Deployed   admin  bacx77     pod-console-logging  default

It’s also useful to list the VLAN and fabric names in the output table. Looking at the JSON again, these values present like this:

"boot_interface": {
            "vlan": {
                "vid": 0,
                "mtu": 1500,
                "dhcp_on": true,
                "external_dhcp": null,
                "relay_vlan": null,
                "secondary_rack": null,
                "name": "untagged",
                "id": 5001,
                "fabric_id": 1,
                "space": "undefined",
                "fabric": "fabric-1",
                "primary_rack": "wnmkpn",
                "resource_uri": "/MAAS/api/2.0/vlans/5001/"
            },

This means they are doubly-nested. No problem; just use double indirection (two levels of . separators) to retrieve them:

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

The modified command yields the desired results:

HOSTNAME      SYSID   POWER  STATUS     OWNER  TAGS                 POOL     VLAN      FABRIC
--------      -----   -----  ------     -----  ----                 ----     ----      ------
lxd-vm-1      r8d6yp  off    Deployed   admin  pod-console-logging  default  untagged  fabric-1
lxd-vm-2      tfftrx  off    Allocated  admin  pod-console-logging  default  untagged  fabric-1
lxd-vm-3      grwpwc  off    Ready      -      pod-console-logging  default  untagged  fabric-1
lxd-vm-4      6s8dt4  off    Deployed   admin  pod-console-logging  default  untagged  fabric-1
lxd-vm-5      pyebgm  off    Allocated  admin  pod-console-logging  default  untagged  fabric-1
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
libvirt-vm-2  kpawad  off    Ready      -      pod-console-logging  default  untagged  fabric-1
libvirt-vm-3  r44hr6  error  Ready      -      pod-console-logging  default  untagged  fabric-1
libvirt-vm-4  s3sdkw  off    Ready      -      pod-console-logging  default  untagged  fabric-1
libvirt-vm-5  48dg8m  off    Ready      -      pod-console-logging  default  untagged  fabric-1
libvirt-vm-6  bacx77  on     Deployed   admin  pod-console-logging  default  untagged  fabric-1

There’s just one more (deeply nested) value we want to retrieve, and that’s the fully-qualified subnet address in CIDR form. That’s a little trickier, because it’s buried in JSON like this:

       "boot_interface": {
            "vlan": {
                "vid": 0,
                "mtu": 1500,
                "dhcp_on": true,
		...
		"resource_uri": "/MAAS/api/2.0/vlans/5001/"
            },
            "parents": [],
            "product": null,
	    ...
	    "link_connected": true,
            "type": "physical",
            "links": [
                {
                    "id": 79,
                    "mode": "auto",
                    "ip_address": "10.124.141.4",
                    "subnet": {
                        "name": "10.124.141.0/24",

So the value we want is in the nested key boot_interface, in a nested array links[], which contains the doubly-nested key subnet.name. We can finish our basic CLI machine list — the one we started with — by adding this complex formulation to the command:

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

Sure enough, this command gives us the same table we had at the beginning of this post:

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

Chaining Ubuntu CLI commands

Although the machine list above looks fairly neat, it’s actually not sorted by hostname, exactly. To accomplish this, we’d need to add a couple of Ubuntu CLI commands to the mix. Sorting on hostname means we want to sort on field 1 of the current command’s output. We can try just feeding that to sort like this:

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 | sort -k 1

This command does indeed sort by hostname:

--------      -----   -----  ------     -----  ----                 ----     ----      ------    ------
HOSTNAME      SYSID   POWER  STATUS     OWNER  TAGS                 POOL     VLAN      FABRIC    SUBNET
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
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  

but it has the unintended side-effect of sorting the header lines into the output. There are probably at least a dozen Ubuntu CLI solutions for this, so we’ll just pick one of the most elegant here, using awk:

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"}'

This command gives us the desired output:

HOSTNAME      SYSID   POWER  STATUS     OWNER  TAGS                 POOL     VLAN      FABRIC    SUBNET
--------      -----   -----  ------     -----  ----                 ----     ----      ------    ------
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
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  

Note that by changing the numerical “-k” argument to “sort,” you can change which field controls the sort:

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 4"}'

This command sorts by machine state, which is the fourth field:

HOSTNAME      SYSID   POWER  STATUS     OWNER  TAGS                 POOL     VLAN      FABRIC    SUBNET
--------      -----   -----  ------     -----  ----                 ----     ----      ------    ------
lxd-vm-2      tfftrx  off    Allocated  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
libvirt-vm-6  bacx77  on     Deployed   admin  pod-console-logging  default  untagged  fabric-1  10.124.141.0/24
lxd-vm-1      r8d6yp  off    Deployed   admin  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-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-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
lxd-vm-3      grwpwc  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

Summary

At this point, it should be clear that jq is a relatively simple, powerful tool for formatting output from the MAAS CLI. You should also remember that, like any Ubuntu CLI command, jq simply outputs text — so anything you can do with text output, you can do with the output from jq. In the next post, we’ll look at some ways to use jq to automatically write CLI scripts to automate various routine MAAS operations.

Ubuntu cloud

Ubuntu offers all the training, software infrastructure, tools, services and support you need for your public and private clouds.

Newsletter signup

Get the latest Ubuntu news and updates in your inbox.

By submitting this form, I confirm that I have read and agree to Canonical's Privacy Policy.

Related posts

A call for community

Introduction Open source projects are a testament to the possibilities of collective action. From small libraries to large-scale systems, these projects rely...

MAAS Outside the Lines

Far from the humdrum of server setups, this is about unusual deployments – Raspberry Pis, loose laptops, cheap NUCs, home appliances, and more. What the heck...

No more DHCP(d)

“He’s dead, Jim.”  Dr. McCoy DHCP is dead; long live DHCP. Yes, the end-of-life announcement for ISC DHCP means that the ISC will no longer provide official...