It is very important to understand how Salt works when running commands from the CLI. The same methodologies can be used when instructing the system to execute commands periodically via Salt’s scheduler or when listening to events and via the reactor, only the syntax is different.
The Salt CLI syntax has the following pattern:
$
sudo salt[
<options>]
<target> <function
>[
<arguments>]
One of the simplest uses is shown in Example 3-1, which executes the test.ping
function on the device identified using the minion ID device1
, without any options or arguments.
$
sudo salt device1 test.ping# output omitted
As we move forward, we’ll analyze each of the components shown here.
Salt is structured as a very simple and pluggable core with many features able to interact between them. A very important functionlity is represented by the execution modules. They are the main entry point into the Salt world. The execution modules are Python modules, and are very easy to read (and eventually write) by anyone with basic Python programming knowledge. Everything is linear, which makes them flexible and easy to understand; in general, they consist only of simple functions.
In Example 3-1 we executed test.ping
: test
is the name of the execution module, while ping
is the execution function inside this module.
There are many module types in Salt and some even have overlapping names, so it is important to always qualify the type of the module. For example, the file state module and the file execution module are different modules and used in different contexts.
This function only returns the logical value True
when the minion is up. test.ping
is an excellent way to test that the minion has been configured correctly and its key is accepted by the master. It doesn’t mean, however, that the connection to the network device succeeded. For this particular usage we can execute the net.connected
module. Repeating the exercise: the Python function connected
is defined under the Salt execution module net
. While the test
execution module corresponds to a Python module called test.py
, net
corresponds to napalm_network.py
. This is because some modules have a virtual name associated and they are loaded under a cross-platform consistent name.
There are many thousands of functions available. For the complete module reference, consult the Salt Module Index.
An execution function must return JSON-serializable data structures. Thanks to this approach the output can be manipulated, ported, and reused in different modules, displayed in the shape we require or transform it and load in a certain service. A good example is on the CLI: the output is transformed into a more human-readable and colorful format. There are many possibilities to adjust the output and this can be pluggable: you even have the option of customizing the way the output is processed and presented.
In Example 3-2, after test.ping
was executed Salt provides the response from device1
as True
.
$
sudo salt device1 test.ping
device1:
True
Another useful function is grains.items
, which provides the complete list of grains, and grains.get
, which returns the value of a given grain identified by name, as shown in Example 3-3.
$
sudo salt device1 grains.get serial
device1:
VM58737E84CF
In a similar way, pillar.items
provides the pillar data available on the minion, while pillar.get
returns a specific value (see Example 3-4).
$
sudo salt device1 pillar.get ntp.servers
device1:
- 172.17.17.1
- 172.17.17.2
Here, the pillar key ntp.servers
has the value of a list of NTP servers. This also introduces the arguments to Example 3-4. ntp.servers
is an argument passed to the get
execution function from the pillar
execution module.
To check what modules are loaded, identified by their virtual name, we can run sys.list_modules
. Similarly, to display the complete list of available functions we can run sys.list_functions
.
For network automation needs there are many execution functions natively embedded in Salt. For example, net.arp
retrieves the ARP tables (see Example 3-5).
$
sudo salt device1 net.arp device1: ---------- out:|
_ ---------- age: 129.0 interface: ae2.100 ip: 10.0.0.1 mac: 00:0f:53:36:e4:50|
_ ---------- age: 1101.0 interface: xe-0/0/3.0 ip: 10.0.0.2 mac: 00:1d:70:83:40:c0
In Example 3-5 we executed the net.arp
function, which retrieves the ARP table from device1
. One can easily notice that the output is not a Python object, though. The reasoning is that although the arp
execution function returned JSON-serializable data, Salt transformed it in a more human-readable and colorful format when displaying on the CLI. This format comes from an outputter module called nested. The user can choose to display the data formatted by a different outputter if they prefer.
The output is a list of ARP entries, each having the following details: age
, interface
, ip
, and mac
. It is vendor-agnostic, thanks to NAPALM capabilities, so executing net.arp
against a device running a different supported platform will return exactly the same structure!
Several other samples are provided in Examples 3-6 through 3-8.
$
sudo salt device3 net.arpinterface
=
TenGigE0/0/0/0# output omitted
Note that although net.arp
does not require an argument in Example 3-5, it can be passed. However, in Example 3-6, we retrieve the ARP table from device3
but only on the interface TenGigE0/0/0/0
.
$
sudo salt device2 ntp.stats 172.17.17.1# output omitted
Example 3-7 returns the NTP synchronization statistics with the server 172.17.17.1
from device2
. The ntp.stats
execution function can be equally executed without an explicit argument, in that case it returns the synchronization details with all the NTP peers.
$
sudo salt device1 net.ping 8.8.8.8vrf
=
CUSTOMER1# output omitted
In Example 3-8 we execute a ping to 8.8.8.8
from the CUSTOMER1
VRF.
You can learn more about these functions in the documentation, but sometimes it’s easier to check it directly from the CLI by executing sys.doc
followed by the function or module name (e.g., salt device1 sys.doc net.arp
). To check which arguments are mandatory and which ones are the default values, you can run sys.argspec
in a similar way (e.g., salt device1 sys.argspec net.arp
).
Targeting is used on the CLI but also in scheduled actions or when reacting to events. It is used to select a group of minions that need to execute a function. Targeting is a task sent by the master in a payload to all minions and the minions decide independently if the target matched their characteristics. This is yet another optimization that makes Salt extremely scalable. In the next sections, we will be executing the test.ping
function.
This is the first targeting method we are exposed to. It’s very easy to understand because it executes the function on a single minion, identified by its ID.
As we have already seen, the command shown in Example 3-9 executes test.ping
only on device1
.
$
sudo salt device1 test.ping# output omitted
As shown in Example 3-10, using the -L
option we can tell the salt
executable to call the function on a list of minions, their IDs being specified as comma-separated values. (It is best to avoid spaces in-between each ID since that format isn’t universally supported throughout Salt.)
$
sudo salt -L device1,device2 grains.get vendor
device2:
Juniper
device1:
Arista
Example 3-10 is going to execute the grains.get
function on device1
and device2
using the vendor
argument. The reply provides the value of the vendor
grain for each device.
The reply order from each device is not necessarily the order we have requested. Salt works asynchronously and the output is displayed as soon as it is received from the minion.
The minions can be selected via their minion ID using shell-style globbing:
Pattern | Explanation |
---|---|
|
Matches everything |
|
Matches any single character |
|
Matches any character in the sequence |
|
Matches any character that is not in the sequence |
The device*
expression from Example 3-11 matches device1
, device2
, and device3
, as these are the Minions authenticated to this Salt master whose ID starts with “device”. If you have thousands of minions with the ID beginning with device
, they all would be matched so you don’t need to write a static list.
$
sudo salt'device*'
grains.get model device1: VMX device2: vEOS device3: ASR9001
Using Perl-compatible regular expressions (PCRE), we can use an even more flexible way to select groups of minions using their ID (see Example 3-12).
$
sudo salt -E'(device|edge)d'
test.ping# output omitted
In this example, the command would be executed on minions starting with either device
or edge
, followed by a digit. Note the usage of the -E
option.
Grains can be also used to target minions using their characteristics, as shown in Examples 3-13 through 3-15 (if you need a refresher on the topic, refer back to “Grains”).
$
sudo salt -G'os:junos'
test.ping# output omitted
$
sudo salt -C'G@os:junos and G@version:15.1F7.3'
test.ping# output omitted
$
sudo salt -C'G@vendor:cisco
and G@model:ASR*
and G@interfaces:TenGigE0/0/0/30'
test.ping# output omitted
Example 3-15 shows that grain matching also accepts globbing (model:ASR*
matches any ASR model). To match grains using regular expressions use the -P
option instead.
In a very similar way it is also possible to target using nested pillar values. For example, we could match the minions that have the hostname ending with as1234.net
(Example 3-16).
$
sudo salt -I'proxy:host:*as1234.net'
test.ping# output omitted
The nesting levels are separated using the :
character. However, this
can also be changed and select a different delimiter, using the --delimiter
option: salt --delimiter='/' -I 'proxy/host/*as1234.net' test.ping
.
This option also allows globbing. To match using regular expressions, use the -J
option.
Everything we just covered can be combined and form a very complex target expression. The command option in that case becomes -C
and the previous options must be specified in the body of the match string using the @
sign (see Examples 3-17 and 3-18).
$
sudo salt -C'device* and G@os:iosxr and G@version:6.*'
test.ping# output omitted
$
sudo salt -C'
P@model:(MX960|MX480)
and J@proxy:host:^(.*bbone.asd+.net)$ '
test.ping# output omitted
For very complex examples such as the one shown in Example 3-18, we can add their definition in the master configuration file along with a name and then use it directly rather than typing the same matching expression each time. These are called nodegroups, and they serve mostly as shortcuts (see Example 3-19).
nodegroups
:
bbone-mxs
:
|
P@model:(MX960|MX480)
and J@proxy:host:^(.*bbone.asd+.net)$
iosxr6
:
|
device*
and G@os:iosxr and G@version:6.*
Which can be used through the -N
CLI option (Example 3-20):
$
salt -N bbone-mxs test.ping# output omitted
$
salt -N iosxr6 test.ping# output omitted
The matching techniques presented can also be used in the top file, where the default match is the compound matcher. We can exploit this flexibility to create smart mappings between the device characteristics and the data entities to be provided.
For example, consider the pillar top file shown in Example 3-21.
base
:
'device*
and
G@os:iosxr
and
G@version:6.*'
:
-
pillar_for_iosxr6
'N@bbone-mxs'
:
-
mx_routers_bbone
This top file includes the pillar_for_iosxr6.sls pillar on minions managing Cisco IOS-XR 6.x devices, and equally the mx_routers_bbone.sls pillar for minions matching the bbone-mxs
nodegroup defined earlier in Example 3-19.
The salt
command has a very long list of options, which you can retrieve by executing salt --help
. In “Targeting Devices” we have listing some of the most usual, the target options. We highly encourage you to explore the full list of options, as they prove very handy in many situations. Due to size limitations in this book, we will present only a few of the most common.
As we discussed earlier, the output from Salt functions is JSON serializable. This output can be transformed in different shapes depending on the application or personal preferences. One can choose using the --out
option between: json
, yaml
, nested
(default), table
, raw
(displays the exact Python object), or pprint
(displays the Python object in a more human-readable form).
$
sudo salt --out=
json device1 net.arp[
{
"interface"
:"ae2.100"
,"ip"
:"10.0.0.1"
,"mac"
:"00:0f:53:36:e4:50"
,"age"
: 129.0}
,{
"interface"
:"xe-0/0/3.0"
,"ip"
:"10.0.0.2"
,"mac"
:"00:1d:70:83:40:c0"
,"age"
: 1101.0}
]
The outputter only displays the Python object on the CLI in a more readable shape. It does not change its structure!
3.142.136.226