iControl REST + jq Cookbook - Part 2: Intermediate
Part 2 deals with slightly complex use cases: Manipulating array elements, looking for strings that match a certain criteria and handling both property names (keys) and values to generate a tabularized output. iControl REST sometimes returns a bare output from the underlying tmsh (e.g., monitor stats). Toward the end, filters for such row strings are discussed. Additionally, methods for converting Unix epoch timestamps are presented.
If you find some jq notations difficult, please revisit the Part 1.
Extract an array element
A single array may contain a large number of elements. For example, the records property of an LTM internal data-group (e.g., tmsh list ltm data-group internal aol
) stores as many entries as you want in a single array. Here's an example from the AOL internal data-group:
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | jq '.records' [ { "name": "64.12.96.0/19", "data": "" }, ... { "name": "207.200.112.0/21", "data": "" } ]
You can selectively extract a single element by specifying the array index number to the records property. The index number starts from 0, so [1]
means the 2nd element.
curl -sku $PASS https://<host>/mgmt/tm/ltm/data-group/internal/aol | jq '.records[1]' { # 0th record element "name": "195.93.16.0/20", "data": "" }
You can specify multiple indexes separated by ,
. In the example below, only the name properties are extracted by additional | .name
.
curl -sku $PASS https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[2, 3, 5] | .name' 195.93.48.0/22 # The name property value from 2nd 195.93.64.0/19 # The name property value from 3rd 198.81.0.0/22 # The name property value from 5th
Note that jq does not raise any error for an out-of-range index: It silently returns null
(just like referencing a non-existing property).
curl -sku $PASS https://<host>/mgmt/tm/ltm/data-group/internal/aol | jq '.records[100]' null
Slice an array
You can extract a subset of array by slicing: Just specify the start (inclusive) and stop (exclusive) indexes separated by colon (:
) inside the []
. For example, to extract the names (IP addresses) from the top 4 elements in the AOL data-group records, run the following command:
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[0:4] | .[].name' 64.12.96.0/19 # The name property value from 0th 195.93.16.0/20 195.93.48.0/22 195.93.64.0/19 # The name property value from 3rd
You might have noticed the subtle difference from the previous comma separated indexes: The additional []
is prepended to the .name
filter. This is because the comma separated indexing (.records[2, 3, 5]
) yields three independent objects (with the name and data properties), while the slice generates an array containing the record objects. Because it is an array, you need to iterate through it to process each record.
When the start index number is omitted, zero is used ([0:4]
and [:4]
are identical). When the end index number is omitted, it assumes the end of the array. The following example extracts the last 4 IP addresses from the AOL data-group (it currently contains 14 records) - from the 10th to the 13th.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[10:] | .[].name' 205.188.146.144/30 # The name property value from 10th 205.188.192.0/20 205.188.208.0/23 207.200.112.0/21 # The name property value from last
Applying the slice to a string yields its substring. For example, you can extract the first 8 characters from "iControlREST" like this:
$ echo '"iControlREST"' | jq '.[:8]' # Piping "iControlREST" to jq "iControl"
Find the element
Having said that, we seldom look for elements by their index numbers. We normally look for a pattern. For example, you want to find the data value of the "useragent6" in the sys_APM_MS_Office_OFBA_DG
internal data-group.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/sys_APM_MS_Office_OFBA_DG | \ jq -r '.records[] | [.name, .data] | join("\t")' ie_sp_session_sharing_enabled 0 ie_sp_session_sharing_inactivity_timeout 60 ofba_auth_dialog_size 800x600 useragent1 microsoft data access internet publishing provider useragent2 office protocol discovery useragent3 microsoft office useragent4 non-browser useragent5 msoffice 12 useragent6 microsoft-webdav-miniredir # Looking for this one useragent7 webdav-miniredir useragent9 ms frontpage 1[23456789] useragent10 onenote
You can extract only the specific element using the select
function. Specify the condition for the selection in the function argument. Usual conditional statements that yields a Boolean can be used: For example, the ==
operator returns true
if two operands are equal.
echo '["BIG-IP", "BIG-IQ"]' | jq '.[] | . == "BIG-IP"' true # BIG-IP is BIG-IP false # BIG-IQ is not BIG-IP
The select
function outputs the input only when the condition in the argument is true.
echo '["BIG-IP", "BIG-IQ"]' | jq '.[] | select(. == "BIG-IP")' "BIG-IP"
With these techniques, you can extract the record (object) that contains the "useragent6" in the name property:
curl -sku $PASS https://<host>/mgmt/tm/ltm/data-group/internal/sys_APM_MS_Office_OFBA_DG | \ jq -r '.records[] | select(.name == "useragent6")' { "name": "useragent6", "data": "microsoft-webdav-miniredir" }
The following comparison operators can be used in jq: !=
, >
, >=
, <
and <=
.
Find matching elements
The select
function accepts anything as the conditional statement as long as it returns true
or false
. For example, the startswith
and endswith
functions. They compare the input against their argument and return a Boolean. Unlike ==
, where you explicitly compare the incoming data (.
) and the string, you only need to specify the string to compare. Let's find the IP addresses that start with "195" in the AOL internal data-group.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[] | .name | select(startswith("195"))' 195.93.16.0/20 195.93.48.0/22 195.93.64.0/19 195.93.96.0/19
Here is an example for endswith
, looking for the "/20" subnets:
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[].name | select(endswith("/20"))' 195.93.16.0/20 198.81.16.0/20 205.188.112.0/20 205.188.192.0/20
Regular expressions? Yes, jq comes with standard regular expression functions such as test
and match
. Here's an example for finding IP addresses that contain 96, dot and any digits:
curl -sku admin:admin https://<host>/mgmt/tm/ltm/data-group/internal/aol | \ jq -r '.records[] | .name | select(test("96.\\d+"))' 64.12.96.0/19 195.93.96.0/19
The jq
regular expression is Perl-compatible, however note that backslashes must be escaped (e.g., \\d+
instead of just \d+
). For more information on the regular expression feature, refer to jq manual.
The RegEx functions are available only when the jq was compiled with the regular expression library Oniguruma. Without the library, you will receive the jq: error (at <stdin>:0): jq was compiled without ONIGURUMA regex libary. match/test/sub and related functions are not available
error upon the use of any RegEx function. The jq on a BIG-IP does not come with the library.
Extract property names and values
Sometimes, you need to obtain both property names (keys) and their values, however, the .[]
filter only yields the values. To get both, filter the incoming objects with the to_entries
function (it acts like the Python's items()
). For example, GET /mgmt/tm/ltm/monitor/http/http
(tmsh list ltm monitor http http
equivalent) returns the following object:
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http | jq '.' { "kind": "tm:ltm:monitor:http:httpstate", "name": "http", ... }
The to_entries
generates an array. Its elements are objects and each object consists of two properties: "key" and "value". The key property keeps the original property name while the value property stores the value of the property.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http | jq 'to_entries' [ { # Object from the 0th property "key": "kind", # 0th property name "value": "tm:ltm:monitor:http:httpstate" # 0th property value }, { "key": "name", "value": "http" }, ... ]
By piping the result to .key, .value
, you now have both property names and values.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http | \ jq -r 'to_entries[] | .key, .value' kind # 0th property name tm:ltm:monitor:http:httpstate # 0th property value name # 1st property name http # 1st property value ...
Obviously, it's much better to have the key and value in the same line. You can do so by using the array constructor []
and the join
function as described in Part 1.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http | \ jq -r 'to_entries[] | [.key, .value] | join(" ... ")' kind ... tm:ltm:monitor:http:httpstate name ... http fullPath ... http generation ... 0 selfLink ... https://localhost/mgmt/tm/ltm/monitor/http/http?ver=15.1.2 adaptive ... disabled adaptiveDivergenceType ... relative ...
Extract a property from a deeply nested object
Some iControl REST responses are deeply nested. For example, the BIG-IP version number is found in the 6th level in a GET /mgmt/tm/sys/version
response:
curl -sku admin:admin https://<host>/mgmt/tm/sys/version | jq '.' { "kind": "tm:sys:version:versionstats", "selfLink": "https://localhost/mgmt/tm/sys/version?ver=15.1.1", "entries": { # top level "https://localhost/mgmt/tm/sys/version/0": { # 2nd level "nestedStats": { # 3rd level "entries": { # 4th level "Build": { # 5th level "description": "0.0.6" # 6th level }, ... "Version": { "description": "15.1.1" # HERE! } ...
Obviously, you can reference the description property in the Version object by chaining the property names with dots from top to bottom (you need to double-quote the URL-like property as it contains sensitive characters).
curl -sku admin:admin https://<host>/mgmt/tm/sys/version | \ jq '.entries."https://localhost/mgmt/tm/sys/version/0".nestedStats.entries.Version.description' "15.1.1"
That's too tedious (and error prone). In this case, you can use []
as a catch-all property for the 2nd, 3rd and 4th levels. As you have seen, the iterator []
yields multiple elements in an array or object, however, because these levels only contain one property, it is as good as referencing specifically.
curl -sku admin:admin https://<host>/mgmt/tm/sys/version | \ jq '.entries[][][].Version.description' "15.1.1"
Tabularize all the version information
Producing a tabularized version information should be easy by now: Combination of the -r
option and the to_entries
and join
functions that you have already seen.
curl -sku admin:admin https://<host>/mgmt/tm/sys/version | \ jq -r '.entries[][][] | to_entries[] | [.key, .value.description] | join("\t")' Build 0.0.6 Date Thu Oct 8 02:52:59 PDT 2020 Edition Point Release 0 Product BIG-IP Title Main Package Version 15.1.1
The .entries[][][]
yields the 5th level objects: e.g., "Build": {"description": "0.0.6"}
. The to_entries
function then generates an object with the key and value properties: The key is "Build" and the "value" is {"description":"0.0.6"} in this case. You can access the data by referencing .key
and .value.description
.
Process raw monitor output
Some iControl REST calls return the raw output from tmsh. For example, GET /mgmt/tm/ltm/monitor/http/http/stats
returns a blob of lines from tmsh show ltm monitor http http
in the.apiRawValues.apianonymous
property.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http/stats | \ jq '.apiRawValues.apiAnonymous' "---------------------------\nLTM::Monitor /Common/http \n------------------ ..."
The output is a single string with the line feeds expressed in the literal "\n" (backslash + n). You can simply output it using the --raw-output
option.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http/stats | \ jq -r '.apiRawValues.apiAnonymous' --------------------------- LTM::Monitor /Common/http --------------------------- Destination: 10.200.20.10:80 State time: up for 8hrs:9mins:13sec | Last error: N/A @2021.01.06 08:44:24 ...
Hmmm ... This is too easy for the intermediate level. Let's achieve the same goal in a harder way: Replace the \n
(2F 6E) with LF (0A) using the string replacement (substitution) function sub
.
sub
requires the regular expression library.
curl -sku admin:admin https://<host>/mgmt/tm/ltm/monitor/http/http/stats | \ jq -r '.apiRawValues.apiAnonymous | sub("\\n"; "\n")' --------------------------- LTM::Monitor /Common/http --------------------------- Destination: 10.200.20.10:80 State time: down for 6hrs:3mins:51sec | Last error: No successful responses received before deadline. @2020.10.30 10:49:33 ...
The first argument of the sub
function is a from string and the second one is a to string. The argument separator is ;
(semicolon). Note that you need to escape the literal backslash by another backslash (\\n
). You can also use regular expressions.
Format 'list sys global-settings' output
Another response that needs massaging might be the one from tmsh list sys global-settings file-blacklist-path-prefix
. As you can see below, it returns a single string, consisting of paths that are enclosed in {}
.
tmsh list sys global-settings file-blacklist-path-prefix sys global-settings { file-blacklist-path-prefix "{/shared/3dns/} {/shared/bin/} {/shared/core/} ... }
Naturally, the equivalent iControl REST call returns a very long string. We can check how long it is by passing the string to the length
function. It returns a number of characters when a string is fed (a number of elements or properties in an array or object):
curl -sku admin:admin https://$HOST/mgmt/tm/sys/global-settings | \ jq '.fileBlacklistPathPrefix | length' 469 # 469 characters long
You can split the string at each whitespace using the split
function (this is not a regular expression function). The function generates an array consisting of substrings.
curl -sku admin:admin https://<host>/mgmt/tm/sys/global-settings | \ jq -r '.fileBlacklistPathPrefix | split(" ")' [ "{/shared/3dns/}", "{/shared/bin/}", "{/shared/core/}", ... ]
If you think the curly braces are obnoxious, you can remove them by feeding it to the gsub
regular expression function: a sibling of sub
that performs substitution globally.
gsub
requires the regular expression library.
curl -sku admin:admin https://<host>/mgmt/tm/sys/global-settings| \ jq -r '.fileBlacklistPathPrefix | split(" ") | .[] | gsub("{|}"; "")' /shared/3dns/ /shared/bin/ /shared/core/ ...
The first argument for gsub
is a from string. Here, a regular expression ("matches either {
or }
") is used. The second argument is a to string. Here, any {
or }
is globally replaced with an empty string. Again, note that the argument separator is ;
. You can use the sub
command for global replacement by specifying the g
(global) flag in the third argument.
curl -sku admin:admin https://<host>/mgmt/tm/sys/global-settings | \ jq -r '.fileBlacklistPathPrefix | split(" ") | .[] | sub("{|}"; ""; "g")'
Convert Unix epoch time
Some time related property values are expressed in Unix epoch in microseconds (10-6). For example, the lastUpdateMicros
and expirationMicros
properties in an authentication token object represent the timestamps for last update and expiration in microseconds.
curl -sku admin:admin https://<host>/mgmt/shared/authz/tokens/LKMRTZ3TZY5PFHJI5HZNRCDUEM | \ jq -r '.lastUpdateMicros, .expirationMicros' 1604038137666701 # Last Update date/time in μs 1604039337667000 # Expiration date/time in μs
You can convert the numbers to human readable format using the todate
function.
curl -sku admin:admin https://<host>/mgmt/shared/authz/tokens/LKMRTZ3TZY5PFHJI5HZNRCDUEM | \ jq -r '.lastUpdateMicros, .expirationMicros | ./(1000*1000) | todate' 2020-10-30T06:08:57Z 2020-10-30T06:28:57Z
The expression in the middle of the filter is an arithmetic computation: It divides the incoming time (.
, which is in microsecond) by 106. This is required because the todate
function expects the Unix epoch time in seconds.
The literal 1000*1000 looks ugly, but it is better than 1000000 (IMHO). If you prefer, you can use the pow
function to compute 10 to the power of negative 6.
$ echo 1604039337667000 | jq '. * pow(10; -6) | todate' "2020-10-30T06:28:57Z"
jq is equipped with standard C math functions (such as cos
and log
). Refer to the jq manual.