Command execution
Note
This page builds upon the Service configuration discussed in Terminal & SFTP/SCP. No additional Service configuration is required for command execution.
Client operation
Single device, single command
The simplest use case for this feature is to execute a command on a device and retrieve the output:
# Assign a device from the service inventory to a variable
>>> csr1 = service.inventory['csr1']
# Execute a single command on that device
>>> response = csr1.exec("show ip int brief")
# Display the output of the command executed
>>> response.data | print
The Device.exec()
call returns an object that is an
instance of radkit_client.sync.exec.SingleExecResponse
, which represents the response
that is returned from the Service for this command on the device.
>>> csr1.exec("show ip int brie")
[PROCESSING] SingleExecResponse(device_name='localhost', command='pwd')
-------------- -----------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
sudo False
data None
errors
-------------- -----------------
The response object starts with a status of PROCESSING
, and will transition
to either SUCCESS
, FAILURE
or PARTIAL_SUCCESS
.
SUCCESS
if the command was run successfully (output is available);FAILURE
if the processing of the request failed completely;PARTIAL_SUCCESS
if multiple commands are performed, but only some of them succeeded.
The RADKit Client is an asynchronous environment: while the request is in PROCESSING
state,
the CLI is not blocked; you can get on with other work (including performing more
requests). Once the response is received, the object simply changes state in the
background.
If however you prefer to synchronously wait for the request to complete, use .wait()
to pause execution of your script, or suspend the REPL prompt, until the request has
completed. Once completed, the data
attribute will contain the response data.
>>> response = csr1.exec("show ip int brie")
# Wait for the request to complete (status will move to SUCCESS)
>>> response.wait()
[PROCESSING] SingleExecResponse(device_name='csr1', command='show ip int brie')
-------------- --------------------------------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid ...
command show ip int brie
data (pending)
-------------- --------------------------------------------------------------------------------
>>> response
[SUCCESS] SingleExecResponse(device_name='csr1', command='show ip int brie')
-------------- --------------------------------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid ...
command show ip int brie
data csr1#show ip int brie\nInterface IP-Address OK? Metho...
-------------- --------------------------------------------------------------------------------
Note
The .wait()
method can take an optional parameter that specifies how many seconds
to wait for. After that time, if the request still hasn’t completed, a TimeoutError
exception will be raised. Note that the request is not affected, and it will still
eventually complete (successfully or unsuccessfully) as if the .wait()
and the
timeout had not happened.
>>> response = csr1.exec("ping 10.1.1.1 repeat 10")
>>> response.wait(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen radkit_client.sync.request>", line 1030, in wait
File "<frozen radkit_client.sync.request>", line 355, in wait
File "/Users/radkit-user/.pyenv/versions/3.9.12/lib/python3.9/concurrent/futures/_base.py", line 448, in result
raise TimeoutError()
concurrent.futures._base.TimeoutError
This is different from the timeout
parameter to .exec()
explained below, which affects the actual command execution.
The request
attribute contains the underlying SimpleRequest
object that
represents the RPC request that was sent to the service. This SimpleRequest
object has attributes like sent_timestamp
.
>>> response.request
[SUCCESS] SimpleRequest(status='SUCCESS', rpc_name='exec-commands')
-------------------- -----------------------------------------------------------
sent_timestamp 2025-03-21 14:15:07
request_type Exec command: 'pwd' on 1ffbf28c-6181-4df1-9e29-7277019c7b6d
client_id None
service_id None
result None
forwarder
e2ee_used True
compression_used zstd
h2_multiplexing_used True
-------------------- -----------------------------------------------------------
Once the request has completed, the actual command output is found under the
.data
property of the SingleExecResponse
object. This is a single
multi-line string.
>>> response.data
'csr1#ping 10.1.1.1 repeat 10\nType escape sequence to abort.\nSending 10, 100-byte ICMP Echos to 10.1.1.1, timeout is 2 seconds:\n..........\nSuccess rate is 0 percent (0/10)\ncsr1#'
This output can be easily processed using standard Python commands, or with the Client’s Pipe commands:
# Print the output as lines
>>> response.data | print
csr1#ping 10.1.1.1 repeat 10
Type escape sequence to abort.
Sending 10, 100-byte ICMP Echos to 10.1.1.1, timeout is 2 seconds:
..........
Success rate is 0 percent (0/10)
csr1#
# Print lines that match a regex
>>> response.data | regrep("Success rate") | print
Success rate is 0 percent (0/10)
Elevated privileges with sudo
If the platform/device allows it (currently we only support this on devices of type Linux
),
you can also execute privileged commands by adding the sudo=True
parameter to the exec
command. It provides the configured Terminal password to the sudo
command:
>>> service.inventory['utm-linux'].exec("whoami", sudo=True).wait().data | print
sudo -k -p xsuzedhood: -- whoami
xsuzedhood:
root
ubuntu2@ubuntu-Standard-PC-Q35-ICH9-2009:~$
The -k
parameter is added in order to always ask for a password, and -p
is added to
use a custom, one-time password prompt that is different for each command (xsuzedhood
in
our example).
Running multiple commands .exec(["whoami", "ls"], sudo=True)
will execute all the
commands successively using sudo
.
Note
Executing commands chained with a semicolon will only execute the first command with
sudo
, the second will be run as the logged-in user. For instance:
# exec("whoami; whoami", sudo=True)
# yields the output of: "sudo whoami; whoami"
root
user
Command timeout
The Device.exec()
call accepts an optional parameter
called timeout
which tells the Service how long (in seconds) it should wait for a command to complete.
If the command does not complete within the specified time, the terminal connection is reset
by the Service, an error response is returned to the Client, and the request goes to
FAILURE
state:
>>> response = csr1.exec("ping 10.1.1.1 repeat 10", timeout=5).wait()
18:06:44.168Z ERROR | command execution failed [device_name='csr1' commands=['ping 10.1.1.1 repeat 10'] error='Device action failed: Performing action failed: Timeout exception while performing commands']
>>> response
[FAILURE] SingleExecResponse(device_name='csr1', command='ping 10.1.1.1 repeat 10')
-------------- -------------------------------------------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid ...
command show ip int brie
data (failed)
errors Device action failed: Performing action failed: Timeout exception while performing commands
-------------- -------------------------------------------------------------------------------------------
In this example, the response object shows that the command execution failed
(FAILURE
) and the errors attribute gives the reason for the failure
(Timeout exception while performing commands
).
>>> response.errors
['command execution failed: Device action failed: Performing action failed: Timeout exception while performing commands']
If one tries to access the command output, an exception is raised:
>>> response.data
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen radkit_client.sync.command>", line 299, in data
File "/Users/radkit-user/.pyenv/versions/3.9.12/lib/python3.9/concurrent/futures/_base.py", line 446, in result
return self.__get_result()
File "/Users/radkit-user/.pyenv/versions/3.9.12/lib/python3.9/concurrent/futures/_base.py", line 391, in __get_result
raise self._exception
File "/Users/radkit-user/.pyenv/versions/radkit-1_0/lib/python3.9/site-packages/anyio/from_thread.py", line 187, in _call_func
retval = await retval
File "<frozen radkit_client.sync.event_loop_thread>", line 120, in coro
File "<frozen radkit_client.sync.command>", line 297, in in_loop
File "<frozen radkit_client.sync.event_loop_thread>", line 64, in result
radkit_client.async_.exec.ExecError: command execution failed: Device action failed: Performing action failed: Timeout exception while performing commands
command execution failed: Device action failed: Performing action failed: Timeout exception while performing commands
Note
This exec
timeout is different from the timeout
parameter to .wait()
explained previously, which has no effect on the actual command execution.
Reset the session
By default the same terminal session will be cached and reused for successive calls to
Device.exec
, for faster response times and reduced
load on the device (no need for a full SSH handshake every time).
The optional parameter reset_before=True
can be used to force clearing the existing cached
terminal connection, if it exists, before the execution of the command. This guarantees that the
command will run on a freshly established terminal connection, with a prompt in a deterministic
state:
>>> response = csr1.exec("ping 10.1.1.1 repeat 10", reset_before=True).wait()
Another option is reset_after=True
which disables the caching of the connection.
This can be used to ensure that the terminal connection state will not be shared with
other Device.exec
requests.
>>> response = csr1.exec("ping 10.1.1.1 repeat 10", reset_after=True).wait()
Single device, multiple commands
Multiple commands can be run by passing a list of commands (list[str]
) instead
of a single one (str
). These commands will be executed sequentially in one sitting.
>>> response = csr1.exec(["show clock", "show ntp association"])
>>>
>>> response.wait()
[SUCCESS] <ExecResponse_OneDevice_ByCommand {2 entries}>
status service_id device command sudo data
-------- ------------ --------- --------------------- ------ ----
SUCCESS None csr1 show clock False ...
SUCCESS None csr1 show ntp association False ...
-------------- -----------------------------------------------------
Note how response
no longer is a single result, but a dict of results, one
per command.
Warning
If the same command is specified multiple times, it will also be
executed multiple times, however, in order to address the specific
outputs individually, the response.by_command[index]
notation needs to
be used.
Individual command outputs are accessed by subscripting response
with the
command string:
>>> response['show clock']
[SUCCESS] SingleExecResponse(device_name='csr1', command='show clock')
----------- --------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid b816846e-21b9-48ae-8efc-6a53af57c3b8
command show clock
sudo False
data csr1#show clock\n14:43:32.937 EDT Thu Aug 11 2022\ncsr1#
errors
----------- --------------------------------------------------------
>>> response['show clock'].data | print
csr1#show clock
14:43:32.937 EDT Thu Aug 11 2022
csr1#
>>> response['show ntp association'].data | print
csr1#show ntp association
address ref clock st when poll reach delay offset disp
*~10.66.141.51 .GNSS. 1 195 1024 377 207.52 -3.862 1.011
~4.4.4.4 .TIME. 16 - 64 0 0.000 0.000 15937.
* sys.peer, # selected, + candidate, - outlyer, x falseticker, ~ configured
csr1#
Note
If a timeout
parameter is provided in an .exec()
statement with multiple
commands, the same timeout applies to every command independently. Currently if
a single command in the set times out, subsequent commands in the list will be skipped,
all commands are considered to have timed out and no output is returned (see below).
If more granularity is required, please use separate .exec()
statements.
>>> response = csr1.exec(["show clock", "ping 10.1.1.1 repeat 10","show ntp association"], timeout=5)
>>> response
{
'show clock': <radkit_client.sync.command.ExecSingleCommandResult object {command='show clock', status='FAILURE'} at 0x117d98d60>,
'ping 10.1.1.1 repeat 10': <radkit_client.sync.command.ExecSingleCommandResult object {command='ping 10.1.1.1 repeat 10', status='FAILURE'} at 0x117d98220>,
'show ntp association': <radkit_client.sync.command.ExecSingleCommandResult object {command='show ntp association', status='FAILURE'} at 0x117d98970>
}
Multiple devices, single command
To run a command on multiple devices, use DeviceDict.exec()
on all or a subset of the Service inventory (see Multiple devices for details).
>>> csrs = service.inventory.filter("name", "csr")
>>> csrs
<radkit_client.sync.device.DeviceDict object at 0x118bb9e50>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS_XE True True False False csr1 True
csr2 172.18.124.38 IOS_XE True True False False False
csr3 172.18.124.39 IOS_XE True True False False False
csr4 172.18.124.55 IOS_XE True True False False False
csr5 172.18.124.42 IOS_XE True True False False False
>>> response = csrs.exec("show ver")
Note how response
is now a mapping of device name to result object. The result
for each command is accessed by subscripting the response with the name of the device:
>>> response
[SUCCESS] <ExecResponse_ByDevice_ToSingle {5 entries}>
status client_id device command sudo data
-------- -------------- -------- --------- ---- -------------------------------------
SUCCESS xxxx-yyyy-zzzz csr1 show ver False csr1#show ver\nCisco IOS XE...
SUCCESS xxxx-yyyy-zzzz csr2 show ver False csr2#show ver\nCisco IOS XE...
SUCCESS xxxx-yyyy-zzzz csr3 show ver False csr3#show ver\nCisco IOS XE...
SUCCESS xxxx-yyyy-zzzz csr4 show ver False csr4#show ver\nCisco IOS XE...
SUCCESS xxxx-yyyy-zzzz csr5 show ver False csr5#show ver\nCisco IOS XE...
>>> response['csr1']
[SUCCESS] SingleExecResponse(device_name='csr1', command='show ver')
----------- ----------------------------------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid b816846e-21b9-48ae-8efc-6a53af57c3b8
command show ver
sudo False
data csr1#show ver\nCisco IOS XE Software, Version 17.06.02\nCisco IOS Software [Ben...
errors
----------- ----------------------------------------------------------------------------------
>>> response['csr2'].data
'csr2#show ver\nCisco IOS XE Software...'
It is then possible to process the data for multiple devices at a time using standard Python syntax such as list/dict comprehensions as well as Client Pipe commands, for example:
>>> { dev: (res.data | regrep('^System image')) for dev, res in response.items() }
{
'csr4': 'System image file is "bootflash:c8000v-universalk9.BLD_POLARIS_DEV_LATEST_20220721_184356.SSA.bin"',
'csr2': 'System image file is "bootflash:c8000v-universalk9.17.07.01a.SPA.bin"',
'csr1': 'System image file is "bootflash:c8000v-universalk9.17.06.02.SPA.bin"',
'csr3': 'System image file is "bootflash:c8000v-universalk9.17.06.02.SPA.bin"',
'csr5': 'System image file is "bootflash:c8000v-universalk9.17.08.01a.SPA.bin"'
}
Partial results and errors
A multi-device request is executed in parallel for all the devices in the set. If an
error occurs for some devices, the response status is set to PARTIAL_SUCCESS
, and
the .status
property of the individual entries shows which commands/devices
failed. Accessing the .data
attribute of the failed entries will result in
an ExecError
which contains the error message as
well.
>>> devices = service.inventory.subset(['csr1', 'asa-1'])
>>> response = devices.exec("show ver").wait()
20:06:48.541Z ERROR | command execution failed [device_name='asa-1' commands=['show ver'] error='Device action failed: Permission error while preparing connection']
>>> response
[PARTIAL_SUCCESS] <ExecResponse_ByDevice_ToSingle {2 entries}>
status service_id device command sudo data
-------- -------------- -------- --------- ---- -----------------------------------------------------------------------------------
FAILURE xxxx-yyyy-zzzz asa-1 show ver False (failed)
SUCCESS xxxx-yyyy-zzzz csr1 show ver False csr1#show ver\nCisco IOS XE Software, Version 17.06.02\nCisco IOS Software [Ben...
Multiple devices, multiple commands
When running a list of commands on multiple devices, you simply have to subscript
.result
twice, first with the device name, then with the command string:
>>> csrs
<radkit_client.sync.device.DeviceDict object at 0x105c63520>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS_XE True True False False csr1 False
csr2 172.18.124.38 IOS_XE True True False False False
>>> response = csrs.exec(['show clock', 'show debug']).wait()
>>> response
[SUCCESS] <ExecResponse_ByDevice_ByCommand {4 entries}>
status service_id device command sudo data
-------- -------------- -------- ---------- ---- ----
SUCCESS xxxx-yyyy-zzzz csr1 show clock False ...
SUCCESS xxxx-yyyy-zzzz csr1 show debug False ...
SUCCESS xxxx-yyyy-zzzz csr2 show clock False ...
SUCCESS xxxx-yyyy-zzzz csr2 show debug False ...
>>> response['csr1']
[SUCCESS] <ExecResponse_OneDevice_ByCommand {2 entries}>
status service_id device command sudo data
-------- -------------- -------- ---------- ---- ----
SUCCESS xxxx-yyyy-zzzz csr1 show clock False csr1#show clock\n10:37:00.618 EDT Fri Aug 12 2022\ncsr1#
SUCCESS xxxx-yyyy-zzzz csr1 show debug False csr1#show debug\nIOSXE Conditional Debug Configs:\n\nConditional Debug Global St...
>>> response['csr1']['show clock']
[SUCCESS] SingleExecResponse(device_name='csr1', command='show clock')
----------- --------------------------------------------------------
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
device csr1
device_uuid b816846e-21b9-48ae-8efc-6a53af57c3b8
command show clock
data csr1#show clock\n10:37:00.618 EDT Fri Aug 12 2022\ncsr1#
errors
----------- --------------------------------------------------------
>>> response['csr1']['show clock'].data
'csr1#show clock\n10:37:00.618 EDT Fri Aug 12 2022\ncsr1#'
Indexing and filtering a response object
The responses in an exec response object can be filtered or addressed by device name, device type, command name or index.
For this, there are .by_device[...]
, .by_device_name
,
.by_command[...]
and .by_index[...]
methods available in all the
response classes. This allows for filtering the results by any of these fields
or simply indexing by a sequence number. These methods can be chained, for
instance:
filtered_response = response.by_device['csr1'].by_status['SUCCESS']
A response object can also be filtered according to a custom condition by
passing a callable to filter()
. This
callable will receive ExecRecord
instances and
should return a boolean True
for records that should be included:
from radkit_client.sync import ExecStatus, ExecRecord
def my_success_filter(entry: ExecRecord[str]) -> bool:
return entry.status == ExecStatus.SUCCESS)
response = response.filter(my_success_filter)
Or in short, using one lambda expression:
response = response.filter(lambda entry: entry.status == ExecStatus.SUCCESS)
Or, by using .by_status[...]
:
response = response.by_status["SUCCESS"]
Note
ExecRecord
is a generic class. The [str]
refers to the type of its data
attribute. This is the output for the
given command. When map()
is
called (see next section), the output can be mapped to a different output,
and so, the type can be different.
Transforming (parsing) a response object
Response objects have a map()
method
that allows for transforming the data
column of all their entries, for
instance by parsing the response string into a data structure.
The returned object is always an
ExecResponse_ByIndex
instance, where the data
column now contains the parsed data. The original data remains accessible
through the raw_data
column.
ExecResponse_ByIndex
is a generic type, where the
type between the square brackets refers to the type of that data
column.
The map function should accept ExecRecord
which
contains:
all information about the individual entry, like a device type, which can be used for lookup up the right parser function;
the data attribute that represents the data that needs to be parsed.
from radkit_client.sync import ExecStatus, ExecRecord, ExecResponse_ByIndex
from typing import Any
def parse_response(entry: ExecRecord[str]) -> dict[str, Any]:
return {
'version': re.findall('[0-9]+', entry.data)[0]
'all_text': entry.data,
}
parsed_response: ExecResponse_ByIndex[dict[str, Any]] = response.map(parse_response)
for response in parsed_response.values():
print("Parsed response: ", response.data)
The map()
method is chainable and
can be combined with filter()
. Both
map()
and
filter()
can be called before the
exec response was returned from the service, and the returned view will be
dynamically populated while the responses arrive.
When the map function raises an exception, the corresponding records will
automatically have a status FAILED
. This can also be used as a way to mark
invalid outputs:
from radkit_client.sync import ExecStatus, ExecRecord, ExecError, ExecResponse_ByIndex
from typing import Any
def map_func(entry: ExecRecord[str]) -> str:
if 'ERROR' in entry.data:
return ExecError("Invalid output.")
return entry.data
mapped_response: ExecResponse_ByIndex[str] = response.map(map_func)
# Print success entries only:
for response in mapped_response.by_status["SUCCESS"]:
print("Parsed response: ", response.data)
Sorting a response object
ExecResponseBase
objects have several sort methods
that allow for sorting the final response view on any of the corresponding
columns:
The returned object is a
ExecResponse_ByIndex
object itself.
In order to sort on two columns, the sort_by
methods can be chained. The
latter sort_by
column in compared first, and if values are identical, the
previous sort_by
column is taken into account.
An example of filtering by status, then sorting by command, then taking the first two results:
response.by_status['SUCCESS'].sort_by_command[:2]