Client user guide
Note
This is a detailed user guide. A short introduction is available at Introduction to RADKit Client. For a simplified Client CLI, check out the RADKit Network Console feature.
Starting RADKit Client
Depending on how you installed the RADKit Client, you can start it using either:
the shortcut created by the installer (Windows or macOS), or
the
radkit-client
command (withpip
installation and macOS/Linux installer).
Command-line arguments
$ radkit-client --help
Usage: radkit-client [OPTIONS] COMMAND [ARGS]...
RADKit Client - version 1.6.0
Options:
--version Display version and exit
--debug Enable debugging
--trace Enable additional debugging
--tracebacks Enables full tracebacks for all exceptions
--light-bg Use darker text colors (for running on a light
background) (DEPRECATED)
--dark-bg Use lighter text colors (for running on a dark
background) (DEPRECATED)
--domain TEXT Set default domain name (default: PROD)
--radkit-directory DIRECTORY Root of the RADKit directories
--settings-file TEXT Path to the custom settings file (can be both
absolute and relative to current working
directory)
-s, --setting TEXT... Override setting
--help Show this message and exit.
Commands:
enroll enroll for client certificate
network-console cli mode
script script and arguments
version display version and exit
Connection to the RADKit Cloud
The RADKit Client uses the HTTPS and Secure WebSocket (WSS) protocols as a transport to communicate with the RADKit Cloud. All TCP connections are outgoing (Client to Cloud). For more information on how to make RADKit Client work through a proxy, please check out Proxy settings.
Choosing a color scheme
Terminal applications don’t have a way of knowing what color scheme is configured in the terminal and what background color is used. To improve the terminal viewing experience the colors can be tuned. By default no adjustment is done - see what works best for you. To store this setting to a configuration file, see Settings management.
# Run Client with slightly lighter colors, for use on a terminal with a
# dark background.
radkit-client -s client.theme light_font
# Run Client with slightly darker colors, for use on a terminal with a
# light background.
radkit-client -s client.theme dark_font
The radkit-interactive tool
With the RADKit Client, the simplest way to connect to a device with SSH/Telnet capability
is to use the radkit-interactive
CLI. This will create an SSH or Telnet session to the
device just like any traditional terminal client.
To display the device names defined for a RADKit Service:
$ radkit-interactive --service-sn xxxx-yyyy-zzzz --sso-email radkit-user@example.com --show-inventory
<radkit_client.sync.device.DeviceDict object at 0x10db0df50>
name host device_type Terminal Netconf SNMP Swagger HTTP description failed
---------------------- ---------------------------------- ------------- ---------- --------- ------ --------- ------ ------------- --------
lab-radkit-cat8kv-1 lab-radkit-cat8kv-1.example.com IOS_XE True True False False False False
lab-radkit-cat9800cl-1 lab-radkit-cat9800cl-1.example.com IOS_XE True True False False False False
lab-radkit-linux-1 lab-radkit-linux-1.example.com LINUX True False False False False False
Untouched inventory from service xxxx-yyyy-zzzz.
To create an SSH or Telnet interactive session with one of the devices in the inventory:
$ radkit-interactive --service-sn xxxx-yyyy-zzzz --sso-email radkit-user@example.com --device-name lab-radkit-linux-1
Attaching to lab-radkit-linux-1 ...
Type: ~. to terminate.
~? for other shortcuts.
When using nested SSH sessions, add an extra ~ per level of nesting.
Warning: all sessions are logged. Never type passwords or other secrets, except at an echo-less password prompt.
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-89-generic x86_64)
Last login: Wed Dec 20 11:31:23 2023 from 10.209.195.246
radkit@lab-radkit-linux-1:~$
To terminate the interactive session, use the SSH escape sequence of ~.
, or the remote device
exit command, e.g. exit
on an Linux host or an IOS XE device.
radkit@lab-radkit-linux-1:~$ exit
logout
The RADKit Client Python interpreter
The RADKit Client is a CLI that runs on top of a Python interpreter, inside what is called
a REPL (Read/Evaluate/Print/Loop) user interface. We use a REPL called ptpython, written by Jonathan Slenders. Feel free to take
a look at the doc for ptpython
to get a feel of what it looks like and how it will
make your life easier while using RADKit, both for interactive use and for automation with
scripting.
Initial scope
When RADKit Client starts up, it provides a few common usage examples to you:
$ radkit-client
<banner removed for brevity>
Example usage:
sso_login("<email_address>") # Authenticate with SSO
certificate_login("<email_address>") # OR authenticate with a certificate
access_token_login("<access_token>") # OR authenticate with an SSO Access Token
service = service_cloud("<serial>") # Then connect to a RADKit Service
service = service_integrated() # Immediately login to an integrated session
service = service_direct() # Establish cloud-less direct connection to service.
service = service_direct_with_sso() # SSO authenticated cloud-less connection.
grant_service_otp() # Enroll a new service
>>>
There are several authentication methods you can use to to create an authenticated client:
SSO/OAuth login (on the default RADKit domains this would be Cisco’s SSO);
Access Token login (when integrating RADKit with solutions that already rely on SSO/OAuth);
certificate-based login (the Client must have been issued a RADKit certificate previously).
For details about these login methods and their parameters, see Stand-alone Client scripts and
the Login section in the API reference. Here is an example of using
sso_login
to create the client connection to the RADKit Cloud:
>>> sso_login("radkit-user@example.com")
Upon successful authentication through SSO, the result of this command is an object that is an
instance of radkit_client.sync.Client
, which represents your authenticated connection
to the RADKit Cloud. Through this connection, you will be able to reach remote RADKit Services.
Note
There are two additional ways to use RADKit Client that do not involve RADKit Cloud:
Integrated mode - the Service and Client run in the same Python process;
direct cloudless login - the Client connects straight to a Service’s webserver, without going through RADKit Cloud.
Object representation
The Client
object, like most objects in RADKit Client,
shows a helpful representation in the interactive CLI when it is the result of an expression typed
at the prompt. The previous command executed sso_login
and stored its result in the
client
variable, so the result of the command was consumed and nothing was displayed.
If now we simply enter client
, the result of that expression is the
object contained in the variable client
, which shows the following:
>>> client
[CONNECTED] Client(status='CONNECTED')
----------------- ----------------------------------------------------
connected_domains PROD (radkit-user@example.com; OIDC auth; authenticated )
services not connected to any service
running_proxies no local proxy running
----------------- ----------------------------------------------------
HINT: use grant_service_otp('<service_owner_email>') to authorize provisioning of a RADKit Service
>>>
The rows in the table correspond to the object’s main characteristics, such as the authentication status, method and domain.
Different object types have different representations, but in general, if you type the
name of a variable that contains a RADKit Client object, you will see something
that looks like the example above. The first line (greyed out in the REPL colored output)
always contains the repr()
of the object, in our example:
<radkit_client.sync.client.Client object {identity='radkit-user@example.com', status='AUTHENTICATED', authentication_method='sso', domain='DEV'} at 0x1064aeeb0>
The repr()
of all RADKit objects look like the one above, with a few important
fields added between curly braces {}
. This way, you can always tell what type of
object you are working with and look it up in the Client API reference if necessary.
Tab completion
The Client REPL provides Tab completion for many different situations. Depending on the
type of object you are dealing with, you may have to use dotted notation (e.g., for object
attributes such as client.params
) or subscript []
notation (e.g., for dictionaries
like service.inventory[...]
or Netconf/YANG models). In most cases, the REPL will be
able to dynamically suggest values that you can enter.
Running Python scripts in the Client
Like most other REPLs as well as the Python interpreter, the RADKit Client can execute one or more scripts during startup. There are two (mutually exclusive) ways to do this:
by setting the
PYTHONSTARTUP
environment variable and/or creatingprofile.py
, orby specifying
script <script> <args> ...
on the command-line, and optionally-i
.
If the PYTHONSTARTUP
environment variable is set and it points to a Python script,
that script will be executed during startup. You can use it for instance to import some
commonly used modules and add variables or objects to the global scope. Other Python
REPLs will also execute PYTHONSTARTUP
if it is defined.
If the script ~/.radkit/client/profile.py
exists, it will be executed
during startup (but only after PYTHONSTARTUP
, if it is also set). You can for instance
invoke sso_login
or certificate_login
and connect to one or more
services that you work with the most, and maybe assign some frequently used devices to variables
or load some Netconf/Swagger capabilities. This file is specific to the RADKit Client and
will not be picked up by other REPLs.
Alternatively, you can specify a script and optional arguments on the radkit-client
command-line:
$ cat myscript.py
import sys
print("Hello, world!")
print("args:", ", ".join(sys.argv))
$ radkit-client script myscript.py one two
<banner removed for brevity>
Hello, world!
args: myscript.py, one, two
That script will be executed at startup with the specified arguments. Note that in this
case, both PYTHONSTARTUP
and profile.py
will be ignored. The same objects
(global scope) available at the REPL prompt will also be available to the script, e.g.,
sso_login
in order to create a client object.
By default, the Client will exit after executing the specified script. If you would
rather be dropped at the REPL prompt after the script completes, use the
--interactive/-i
command line parameter. All the objects created by the script
in the global scope will then be available to you.
$ cat myscript.py
sso_login("radkit-user@example.com")
service = service_cloud("xxxx-yyyy-zzzz").wait()
$ radkit-client script -i myscript.py
<banner removed for brevity>
Example usage:
sso_login("<email_address>") # Authenticate with SSO
certificate_login("<email_address>") # OR authenticate with a certificate
access_token_login("<access_token>") # OR authenticate with an SSO Access Token
service = service_cloud("<serial>") # Then connect to a RADKit Service
service = service_integrated() # Immediately login to an integrated session
service = service_direct() # Establish cloud-less direct connection to service.
service = service_direct_with_sso() # SSO authenticated cloud-less connection.
grant_service_otp() # Enroll a new service
Running startup script(s):
- myscript.py
21:49:11.472Z INFO | Connecting to forwarder [uri='wss://prod.radkit-cloud.cisco.com/forwarder-2/websocket/']
21:49:11.780Z INFO | Connection to forwarder successful [uri='wss://prod.radkit-cloud.cisco.com/forwarder-2/websocket/']
21:49:14.371Z INFO | Connecting to forwarder [uri='wss://prod.radkit-cloud.cisco.com/forwarder-1/websocket/']
21:49:14.682Z INFO | Connection to forwarder successful [uri='wss://prod.radkit-cloud.cisco.com/forwarder-1/websocket/']
>>>
Warning
This way of running scripts is intended to provide an easy way to turn commands from an interactive session into a script for quick prototyping. However, it is not the recommended way to program using RADKit Client. See Stand-alone Client scripts for a guide on how to best write automation using the Client API.
RADKit Client objects
Object categories
Warning
As a general rule, none of the RADKit Client classes should ever be instantiated manually. If you need an object, there is always a function, method, attribute… that will create and/or return it for you. If you do not follow this rule, the internal state of the Client will become inconsistent, resulting in hard to track errors or/and other misbehavior.
RADKit Client defines many classes of objects. In the beginning, you will mostly encounter objects such as:
radkit_client.sync.client.Client
: represents your authenticated connection to the RADKit Cloud. This requires a successful authentication with one of the supported authentication methods beforehand, e.g.,sso_login
orcertificate_login
.Note
It is possible to perform multiple logins, possibly on different RADKit domains (cloud environments) and end up with multiple
Client
objects, each representing a separate authenticated connection to a specific RADKit domain. This should however be done with care, as it would make it easier to leak sensitive information from one domain to the other.radkit_client.sync.service.Service
: represents a RADKit Service instance. Typically the Service runs in a lab or on a customer network, in which case it is accessed through the RADKit Cloud. You can have one or moreService
objects active, depending on whether you are working on one or more remote networks at the same time.radkit_client.sync.device.Device
: represents a single device that is part of the inventory of a given service. EachDevice
is tied to a specificService
which is itself tied to yourClient
, which is bound to your user identity and a specific RADKit domain.Request objects: in RADKit Client, every action carried out over the network is represented by a request object. There are different sorts of requests, such as inventory update request, command execution request, Netconf capabilities request, etc. For example, a
radkit_client.sync.service.Service.update_inventory()
request is represented by aradkit_client.sync.request.SimpleRequest
object.Result objects: all requests will have an associated result. Some results have a
data
property, some do not (e.g., a service inventory update triggers a change in the service object, but there is no specific value to be stored as the result data, while for command execution, thedata
property would contain the output of the command). These result objects vary depending on the type of request.
Object status
Most objects in RADKit Client have a status, which can take values such as:
PROCESSING
: the object is busy carrying out an action (initializing, or waiting for a response of some kind).SUCCESS
: the object has completed its action successfully.PARTIAL_SUCCESS
: the object completed its action but there were some failures (for instance, a multi-device request that failed for one or more devices).FAILURE
: the object has entirely failed to complete its action (for instance, it got an error response or encountered an exception).
The object’s status is accessed through the .status
attribute. Depending on the type
of the object, there will be different possible statuses with specific meanings, some commonly
used ones are:
RequestStatus
NetconfAPIStatus
SwaggerAPIStatus
If an object has a status, it is shown before the repr()
for that object, on the
first line of the object’s interactive CLI display:
>>> client.requests[1]
[SUCCESS] <radkit_client.sync.request.SimpleRequest object {status='SUCCESS'} at 0x106fec580>
-------------- ----------------------------------------------
sent_timestamp 2022-07-11 15:17:00
request_type Get service capabilities
identity radkit-user@example.com
service_serial xxxx-yyyy-zzzz
result ServiceCapabilities()
forwarder wss://prod.radkit-cloud.cisco.com/forwarder-1/
-------------- ----------------------------------------------
Waitable objects
Many objects are also waitable, i.e. you can wait for them to complete – which can mean different things for different objects. For some objects, it may mean that the initialization phase is complete. For others, that some expected response has come in.
Note
For those familiar with asyncio
, this is not the same thing as await
.
RADKit Client is a highly asynchronous environment, where you can issue a request
to a remote Service and do something else while waiting for the response. However, there
are times where you just need the result to come in before you continue, or you need to
ensure that things are done in a certain sequence.
This is where you will mostly rely on .wait()
.
If instead of waiting for an object, you prefer to check its status in real time, you can type the name of the variable containing the object at the prompt and look at its status on the first line:
>>> req = service.inventory['csr1'].exec("ping 1.1.1.10")
>>> req
[DELIVERED] <radkit_client.sync.request.TransformedFillerRequest object at 0x10cf15910>
...
>>> req
[DELIVERED] <radkit_client.sync.request.TransformedFillerRequest object at 0x10cf15910>
...
>>> req
[DELIVERED] <radkit_client.sync.request.TransformedFillerRequest object at 0x10cf15910>
...
>>> req
[SUCCESS] <radkit_client.sync.request.TransformedFillerRequest object at 0x10cf15910>
...
The .wait()
method returns the object that it was run on, which means it can be
chained with other object methods. For instance, the .result.data
attribute of a
request is not available until the request has completed, but it can be chained to the
.wait()
method:
>>> req = csr1.exec('ping 2.2.2.20 re 10')
>>> req.wait().result.data
(some time elapsed...)
'csr1#ping 2.2.2.20 re 10\nType escape sequence to abort.\nSending 10, 100-byte ICMP Echos to 2.2.2.20,
timeout is 2 seconds:\n..........\nSuccess rate is 0 percent (0/10)\ncsr1#'
Note that .wait()
can take an optional timeout: float
parameter that tells it
to wait for up to timeout
seconds for the object to complete. After that time, if
the object still isn’t complete, a TimeoutError
is raised and execution resumes.
Service object
One connects to a remote RADKit Service by providing its serial number (found at the top
of the Service Web UI page) to Client.service_cloud()
:
>>> service = client.service_cloud("xxxx-yyyy-zzzz")
15:41:16.019Z INFO | Connecting to forwarder [uri='wss://prod.radkit-cloud.cisco.com/forwarder-4/websocket/']
15:41:16.515Z INFO | Connection to forwarder successful [uri='wss://prod.radkit-cloud.cisco.com/forwarder-4/websocket/']
15:41:16.929Z INFO | Connecting to forwarder [uri='wss://prod.radkit-cloud.cisco.com/forwarder-1/websocket/']
15:41:17.240Z INFO | Connection to forwarder successful [uri='wss://prod.radkit-cloud.cisco.com/forwarder-1/websocket/']
>>>
>>> service
------------- ----------------
[READY] Service(name='xxxx-yyyy-zzzz', service_id='xxxx-yyyy-zzzz')
----------------------------- ---------------------------------------------------------
name xxxx-yyyy-zzzz
domain PROD
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
#devices 11
#capabilities 18
version 1.8.0
e2ee_supported Yes, required
e2ee_active Yes, using E2EE. (E2EE setting is set to WHEN_AVAILABLE).
e2e_verify_required No
supported_compression_methods ['zstd']
supports_h2_multiplexing Yes
connection_method None
direct_rpc_url None
----------------------------- ---------------------------------------------------------
Inventory of devices
The Service’s inventory (list of devices and a few basic attributes) is retrieved upon
creation of the Service
object. It is displayed and
accessed using Service.inventory
:
>>> service.inventory
<radkit_client.sync.device.DeviceDict object at 0x10c2eb190>
name host device_type Terminal Netconf Swagger HTTP description failed
--------- -------------- ------------- ---------- --------- --------- ------ ------------- --------
asa-1 172.18.124.217 ASA True False False False False
asa-2 172.18.124.56 ASA True False False False False
c8kv1 172.18.124.43 IOS_XE True False False False False
cEdge-1 172.18.124.208 CEDGE True False False False cEdge-1 False
cEdge-2 172.18.124.209 CEDGE True False False False cEdge-2 False
cEdge-3 172.18.124.210 CEDGE True False False False cEdge-3 False
csr1 172.18.124.37 IOS_XE True True False False False
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
ubuntu-1 10.122.149.118 LINUX True False False False False
ubuntu-2 172.18.124.126 LINUX True False False False False
ubuntu-3 10.122.149.90 LINUX True False False False False
vManage-1 172.18.124.60 VMANAGE False False True True vManage False
Note
The Service
object is a waitable object
(see Waitable objects above) that goes from incomplete to complete once its
capabilities and inventory have been downloaded.
It is good practice when scripting, to add .wait()
after creating the Service
object so that the inventory is guaranteed to be populated:
>>> service = client.service_cloud("xxxx-yyyy-zzzz").wait()
>>> csr1 = service.inventory['csr1']
If the inventory has changed on the remote side, it can be retrieved again using
Service.update_inventory()
.
The method returns a radkit_client.sync.request.SimpleRequest
object that can be waited for.
>>> service.update_inventory().wait()
[SUCCESS] SimpleRequest(status='SUCCESS', rpc_name='get-basic-inventory')
-------------------- ------------------------------------------------------------
sent_timestamp 2025-02-06 14:18:37
request_type Service inventory
client_id radkit-user@example.com
service_id xxxx-yyyy-zzzz
result InventoryUpdateResult(total_devices=15, devices_added=0, d...
forwarder wss://prod.radkit-cloud.cisco.com/forwarder-2/
e2ee_used True
compression_used zstd
h2_multiplexing_used True
-------------------- ------------------------------------------------------------
Note
RADKit does not currently support automatic updates of the inventory in the Client when devices are added/removed/modified on the remote Service. This will be added in a future release. For now, please update manually.
Device Object
Each device in the inventory of the remote Service is represented in the Client by a
Device
object that is accessed by subscripting
the Service inventory with the device name:
>>> service.inventory['csr1']
<radkit_client.sync.device.Device object {name='csr1', host='172.18.124.37', device_type='IOS_XE', forwarded_tcp_ports='', failed=False} at 0x10a8af5e0>
Object parameters
-------- ----------------
identity radkit-user@example.com
serial xxxx-yyyy-zzzz
name csr1
-------- ----------------
Internal attributes
key value
------------------- -------------
description
device_type IOS_XE
forwarded_tcp_ports
host 172.18.124.37
http_config False
netconf_config True
snmp_version None
swagger_config False
terminal_config True
External attributes
APIs
------- -------
Netconf UNKNOWN
Swagger UNKNOWN
------- -------
Parameters and attributes
The Device
object has three important sets of attributes:
Device.params
are theObject parameters
in the output above. The most important parameter isname
, which is the display name as well as the user-facing unique identifier for that device on the Service side. The other two are theidentity
andserial
parameters, which are identical to those of the device’s parentradkit_client.sync.client.Service
object.Note
For convenience,
<device>.name
is provided as an alias to<device>.params.name
.Device.attributes.internal
are additional attributes that are part of the Service inventory. Those include the device type, description, as well as boolean fields indicating whether certain protocols are configured for that device. For example, ifterminal_config
isTrue
, it means that the Service has a terminal (SSH/Telnet) credentials configured for the device (note that this does not necessarily mean that the device is actually reachable over these protocols).Device.attributes.metadata
are attributes which are retrieved from an external source. If the Service has imported the device from an external source such as Catalyst Center, it has the possibility of querying additional metadata from that source. Those external attributes are not retrieved as part of the inventory, instead they have to be fetched usingDevice.update_attributes()
.
Note
You will notice that device parameters are accessed using dotted notation, while
device attributes are accessed using subscript notation []
. This is because the
names of the parameters are fixed, as they are fundamental properties of the object,
while attributes are received from a remote source, therefore their names are not
known in advance.
# Dotted notation
>>> csr1.params.name
'csr1'
# Subscript notation
>>> csr1.attributes.internal['device_type']
'IOS_XE'
Device renaming
A device can be renamed on the Service. Once the Service inventory is refreshed from
the Client, the new name is reflected in the inventory itself, as well as all the
device dictionaries that the device belongs to (see next section), and of course in the
parameters of the Device
object for the renamed device:
# Assign Device object to a variable
>>> dev = service.inventory['before']
>>> csr1.params.name
'csr1'
>>> csr1
<radkit_client.sync.device.Device object {name='csr1', host='172.18.124.37', device_type='IOS_XE', forwarded_tcp_ports='', failed=False} at 0x10ab6a2e0>
Object parameters
-------- ----------------
identity radkit-user@example.com
serial xxxx-yyyy-zzzz
name csr1
-------- ----------------
...
#
# Rename device from "csr1" to "csr1-new" on Service, then update inventory
#
>>> req = service.update_inventory().wait()
>>> csr1.params.name
'csr1-new'
>>> csr1
<radkit_client.sync.device.Device object {name='csr1-new', host='172.18.124.37', device_type='IOS_XE', forwarded_tcp_ports='', failed=False} at 0x10ab6a2e0>
Object parameters
-------- ----------------
identity radkit-user@example.com
serial xxxx-yyyy-zzzz
name csr1-new
-------- ----------------
...
Multiple devices
Many of the operations that can be performed on a single device may also be performed on the complete Service inventory. For instance, to download external attributes for all devices in one go, you can do:
>>> service.inventory.update_attributes().wait()
However, for most actions such as command execution or Netconf queries, it is usually not
desirable to perform those on all devices, but rather only on a subset. That can be
achieved by building a radkit_client.sync.device.DeviceDict
, which is a mutable collection
of Device
objects that can be added/removed as needed.
A DeviceDict
essentially behaves like a set
in
Python, but it can also be superscripted like a dict
using square brackets [...]
.
Copy, subset and filter
A DeviceDict
can be
built from the Service inventory or from another device dict using one of these methods:
DeviceDict.copy()
: returns a mutable copy of the original device dict;DeviceDict.subset()
: returns a subset of a device dict based on a list ofname
values orDevice
objects;DeviceDict.filter()
: returns a subset of a device dict that matches a certain pattern (regex) for a certain device parameter or attribute.
# Filter devices based on name
>>> service.inventory.filter('name', 'csr')
<radkit_client.sync.device.DeviceDict object at 0x11467ffd0>
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
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
# Keep devices that have Swagger credentials configured
>>> service.inventory.filter("swagger_config", "True")
<radkit_client.sync.device.DeviceDict object at 0x114d4df40>
name host device_type Terminal Netconf Swagger HTTP description failed
--------- ------------- ------------- ---------- --------- --------- ------ ------------- --------
vManage-1 172.18.124.60 VMANAGE False False True True vManage False
Adding and removing
A DeviceDict
can be added or removed to using:
DeviceDict.add()
: adds one or moreDevice
objects to the device dict (one may either add a single device object, a list of device objects, or another device dict);DeviceDict.remove()
: works like.add()
, but removes the devices instead.
Note
You can .add()
by simply specifying the device name, you don’t necessarily have to
pass an actual Device
object. The Client is
able to find the device by name in the inventory, as shown in the example below.
>>> my_csrs = service.inventory.filter('name', 'csr')
>>> my_csrs
<radkit_client.sync.device.DeviceDict object at 0x115285e20>
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
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
>>> my_csrs.remove('csr1')
>>> my_csrs
<radkit_client.sync.device.DeviceDict object at 0x115285e20>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
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
>>> my_csrs.add('csr1')
>>> my_csrs
<radkit_client.sync.device.DeviceDict object at 0x115285e20>
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
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
Set operations
A DeviceDict
can also be manipulated using the
standard Python set
operations.
>>> a = service.inventory.copy()
>>> b = service.inventory.copy()
>>> a.remove(['csr4', 'csr5'])
>>> b.remove(['csr1', 'csr2'])
>>> a
<radkit_client.sync.device.DeviceDict object at 0x10eaa9090>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
>>> b
<radkit_client.sync.device.DeviceDict object at 0x10edd1360>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr3 172.18.124.39 IOS False False False False False
csr4 172.18.124.55 IOS False False False False False
csr5 172.18.124.42 IOS False False False False False
You can use most set operators such as
AND &
, OR |
and XOR ^
on a
and b
to perform set operations
on them and get back a resulting DeviceDict
:
>>> a & b # (intersection)
<radkit_client.sync.device.DeviceDict object at 0x10fd6dde0>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr3 172.18.124.39 IOS False False False False False
>>> a | b # (union)
<radkit_client.sync.device.DeviceDict object at 0x10fdd0070>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
csr4 172.18.124.55 IOS False False False False False
csr5 172.18.124.42 IOS False False False False False
>>> a ^ b # (symmetric difference)
<radkit_client.sync.device.DeviceDict object at 0x10fd4c430>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
csr4 172.18.124.55 IOS False False False False False
csr5 172.18.124.42 IOS False False False False False
>>> a - b # (difference)
<radkit_client.sync.device.DeviceDict object at 0x10fbbd0c0>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
Singleton
A DeviceDict
can also be built from a single
device object using Device.singleton()
, which
returns a dict containing only that device; other devices can then be added to it:
>>> csr1 = service.inventory['csr1']
>>> my_csrs = csr1.singleton()
>>> my_csrs
<radkit_client.sync.device.DeviceDict object at 0x1159a8430>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS_XE True True False False csr1 False
>>> my_csrs.add(['csr2', 'csr3'])
>>>
>>> my_csrs
<radkit_client.sync.device.DeviceDict object at 0x1159a8430>
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
csr3 172.18.124.39 IOS_XE True True False False False
Inventory tracking
The service.inventory
property behaves in a particular way, in that it returns a different
DeviceDict
instance every time it is called
(note the different object IDs below):
>>> id(service.inventory)
4518264304
>>> id(service.inventory)
4549064688
The DeviceDict
returned by service.inventory
is
initially “untouched”, i.e. it tracks the Service inventory and will reflect any changes
(devices enabled, disabled, added or removed) carried out on the Service side and fetched by
update_inventory()
.
However, as soon as this device dict is “touched” in any way (devices added or removed), it
stops tracking the inventory i.e. newly added devices on the Service won’t automatically show up
in this device dict anymore. This also applies to copies or subsets of the original dict,
e.g. service.inventory.copy()
returns a device dict that will not track additions to the
inventory.
All DeviceDict
objects will stop
displaying a device if it is temporarily disabled (hidden from view) on the Service. Any actions
carried out on the devices in the dict during that time will not include the disabled device.
Once the device is re-enabled, it will automatically re-appear in the dict. The disabled device
will however not be carried over to other dicts (through e.g. copy()
, filter()
or
set operations like &
or |
) while it is disabled.
>>> a
<radkit_client.sync.device.DeviceDict object at 0x10eaa9090>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
# Disable 'csr2' in the Service WebUI and refresh
>>> service.update_inventory().wait()
>>> a
<radkit_client.sync.device.DeviceDict object at 0x10eaa9090>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
# Take a copy while 'csr2' is disabled
>>> b = a.copy()
# Re-enable 'csr2' in the Service WebUI and refresh
>>> service.update_inventory().wait()
# The disabled device has re-appeared in a
>>> a
<radkit_client.sync.device.DeviceDict object at 0x10eaa9090>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr2 172.18.124.38 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
# The disabled device was not carried over to b
>>> b
<radkit_client.sync.device.DeviceDict object at 0x10eaa90ff>
name host device_type Terminal Netconf Swagger HTTP description failed
------ ------------- ------------- ---------- --------- --------- ------ ------------- --------
csr1 172.18.124.37 IOS False False False False False
csr3 172.18.124.39 IOS False False False False False
Request object
Every operation performed on a remote Service is represented by a request object.
The interactive CLI display of a request object shows all the parameters for that request. Some parameters
like req_type
and sent_timestamp
are always present; the others depend on the type of request.
>>> req = service.update_inventory()
>>> req
[SUCCESS] <radkit_client.sync.request.SimpleRequest object {status='SUCCESS'} at 0x115b6c760>
-------------- ------------------------------------------------------------
sent_timestamp 2022-07-22 12:07:11
request_type Service inventory
identity radkit_user@example.com
service_serial xxxx-yyyy-zzzz
result InventoryUpdateResult(total_devices=15, devices_added=0, ...
forwarder wss://prod.radkit-cloud.cisco.com/forwarder-4/
-------------- ------------------------------------------------------------
Note
If for some reason you need to check the type of a request in your code, do not
rely on request_type
which is for display only; use isinstance()
instead.
All request
objects have a result
property, which returns a different
type/value depending on the type of request and/or its parameters.
History of requests
It is easy to lose a request
object by forgetting to assign it to a
variable, or by overwriting the variable it was stored in. This can be especially
frustrating if the request may not be performed a second time without consequences
(for instance, when reading and clearing a set of counters in a single command).
There are two convenient ways to retrieve a lost request object:
1) The _
(underscore) recalls the result of the last expression evaluated in the
REPL. It can be used to retrieve a request that was just issued but was not assigned to
a variable:
>>> csr1.exec('show clock')
[NOT_YET_SUBMITTED] <radkit_client.sync.request.TransformedFillerRequest object at 0x11ba59df0>
-------------- -----------------
sent_timestamp None
request_type Command execution
identity radkit_user@example.com
service_serial xxxx-yyyy-zzzz
result None
forwarder
-------------- -----------------
>>> _
[SUCCESS] <radkit_client.sync.request.TransformedFillerRequest object at 0x11ba59df0>
-------------- --------------------------------------------------------------------------------
sent_timestamp 2022-07-22 16:28:46
request_type Command execution
identity radkit_user@example.com
service_serial xxxx-yyyy-zzzz
result <radkit_client.sync.command.ExecSingleCommandResult object {command='show clock', ...
forwarder wss://prod.radkit-cloud.cisco.com/forwarder-4/
-------------- --------------------------------------------------------------------------------
Note
Be careful that if you access attributes such as _.status
, the result of that expression will
be stored into _
and you will lose the reference to the request object. Better
get into the habit of doing req = _
and req.status
instead.
2) All Client
,
Service
and Device
objects have a .requests
property that returns a
radkit_client.sync.request_store.RequestDict
of all the past requests associated with
that object, indexed by a numerical key
. The subscript []
notation can be used to retrieve
the desired request:
>>> csr2.requests
<radkit_client.sync.state.RequestsDict object at 0x115e09950>
Request objects for device: csr2
key status sent_timestamp request_type updates result
----- -------- ------------------- -------------------- ----------------- --------------------------------------------------------------------------------
0 SUCCESS 2022-07-22 16:43:36 Command execution 1 total, 0 failed {'csr2': {'show ver': <radkit_client.sync.command.ExecSingleCommandResult object {...
1 SUCCESS 2022-07-22 16:43:53 Netconf capabilities None
Pipe commands
The Python syntax is great when coding, but not necessarily the most convenient when working interactively. The RADKIT Client REPL provides a few helpers in the form of pipe commands, which are similar to UNIX Shell pipes. A few examples are provided below, and a complete reference can be found in the API reference.
Note
You may not see an immediate use for these pipe commands. However, these will come in handy when working with features such as Exec, Netconf, or Swagger (see Feature guides below).
Simple text processing
A very common piece of data in the context of RADKIT is an str
object containing
multiple lines of text, eg. the output of some command:
>>> output
'csr3#show ip int brie\nInterface IP-Address OK? Method Status Protocol\nGigabitEthernet1 172.18.124.39 YES NVRAM up up\nGigabitEthernet2 22.1.1.2 YES NVRAM up up\nTunnel10 45.1.2.2 YES manual administratively down down\ncsr3#'
>>>
Common operations can be performed on that multi-line string using pipes, such as
print
, grep
, head
or sort
:
print
| print
Prints the string to standard output. If the output is longer than a screenful, it is paginated (press Enter or Space to scroll).
>>> output | print
csr3#show ip int brie
Interface IP-Address OK? Method Status Protocol
GigabitEthernet1 172.18.124.39 YES NVRAM up up
GigabitEthernet2 22.1.1.2 YES NVRAM up up
Tunnel10 45.1.2.2 YES manual administratively down down
csr3#
grep
| grep(<string>, [<ignore_case>], [<inverse>])
Looks for lines containing <string>
in the multi-line string on the left and returns
a new multi-line string containing only those lines. If <ignore_case>
is True
(the default), the search is not case-sensitive; set it to False
to match case.
If <inverse>
is True
, those lines not matching the string are returned.
# String search (case-insensitive by default)
>>> output | grep ("tunnel")
'Tunnel10 45.1.2.2 YES manual administratively down down'
# String search for lines that does **not** contain matched string
>>> output | grep ("tunnel", inverse=True) | print
csr3#show ip int brie
Interface IP-Address OK? Method Status Protocol
GigabitEthernet1 172.18.124.39 YES NVRAM up up
GigabitEthernet2 22.1.1.2 YES NVRAM up up
regrep
| regrep(<regex>, [<inverse>])
Looks for lines matching <regex>
in the multi-line string on the left and returns a
new multi-line string containing only those lines. For a detailed Python regex syntax
reference, please see the re
module documentation.
If <inverse>
is True
, those lines not matching the regex are returned.
# Regex search
>>> output | regrep(r'^Gig') | print
GigabitEthernet1 172.18.124.39 YES NVRAM up up
GigabitEthernet2 22.1.1.2 YES NVRAM up up
# Case-insensitive regex search
>>> output | regrep(r'(?i)^gig') | print
GigabitEthernet1 172.18.124.39 YES NVRAM up up
GigabitEthernet2 22.1.1.2 YES NVRAM up up
head
| head([<n>])
Takes the specified number <n>
of lines (10 by default) at the start of the
multi-line string on the left and returns a new multi-line string containing those
lines. If the number <n>
is negative, all lines are returned except the last
<n>
. If <n>
is omitted, you can just type | head
without ()
.
# First 2 lines only
>>> output | head(2) | print
csr3#show ip int brie
Interface IP-Address OK? Method Status Protocol
tail
| tail([<n>])
Same as | head
but starts from the end of the multi-line string.
# Last 2 lines only
>>> output | tail(2) | print
Tunnel10 45.1.2.2 YES manual administratively down down
csr3#
# Skip the first 3 lines
>>> output | tail(-3) | print
GigabitEthernet2 22.1.1.2 YES NVRAM up up
Tunnel10 45.1.2.2 YES manual administratively down down
csr3#
sort
| sort([<reverse>])
Sorts the lines from the multi-line string on the left and returns a new
multi-line string containing the sorted lines.
If <reverse>
is True
, the lines are sorted in reverse order.
>>> output | sort | print
GigabitEthernet1 172.18.124.39 YES NVRAM up up
GigabitEthernet2 22.1.1.2 YES NVRAM up up
Interface IP-Address OK? Method Status Protocol
Tunnel10 45.1.2.2 YES manual administratively down down
csr3#
csr3#show ip int brie
Note
The lines are sorted alphabetically (first digits, then uppercase, then lowercase).
Structured data processing
to_json
| to_json([kwargs])
Serializes the object on the left into its JSON string representation. If any keyword
arguments are provided, those are passed to json.dumps
(see the json
module
documentation for details).
>>> d = {'a':1, 'b':2, 'c':None}
>>> d
{"a": 1, "b": 2, "c": None}
>>> d | to_json
'{\n "a": 1,\n "b": 2,\n "c": null\n}'
>>> d | to_json | print
{
"a": 1,
"b": 2,
"c": null
}
>>> d | to_json(indent=2) | print
{
"a": 1,
"b": 2,
"c": null
}
>>> d | to_json | regrep('"b"') | print
"b": 2,
Note
The output of | to_json
is a multi-line string, thus it can be used as the
left-hand side argument to e.g., | regrep()
.
from_json
| from_json([kwargs])
Parses the JSON string on the left and converts it into a Python object. If any keyword
arguments are provided, those are passed to json.loads
(see the json
module
documentation for details).
>>> d | to_json | from_json
{"a": 1, "b": 2, "c": None}
>>> (d | to_json | from_json)["a"]
1
to_dict
| to_dict
Turns an RADKIT Client object into a Python dictionary. The way this is done depends on the object type, but generally, it will match the tabular structure that is shown in the interactive representation of the object.
# RADKIT device parameters (dataclass)
>>> csr1.params
<radkit_client.sync.device.DeviceParameters object {identity='radkit-user@example.com', serial='xxxx-yyyy-zzzz', name='csr1'} at 0x118b10670>
-------- ----------------
identity radkit-user@example.com
serial xxxx-yyyy-zzzz
name csr1
-------- ----------------
>>> csr1.params | to_dict
{'identity': 'radkit-user@example.com', 'serial': 'xxxx-yyyy-zzzz', 'name': 'csr1'}
# Service inventory (each radkit_client.sync.device is turned into a nested dictionary)
>>> asas = service.inventory.filter("name", "asa")
>>>
>>> asas | to_dict
{'asa-1': {'name': 'asa-1', 'host': '172.18.124.217', 'device_type': 'ASA', 'failed': False, 'swagger': {'status': 'UNKNOWN'}, 'netconf': {'status': 'UNKNOWN'}},
'asa-2': {'name': 'asa-2', 'host': '172.18.124.56', 'device_type': 'ASA', 'failed': False, 'swagger': {'status': 'UNKNOWN'}, 'netconf': {'status': 'UNKNOWN'}}}
write_to_file
| write_to_file([<file_path>])
Takes a multi-line string on the left and writes it into a file. If a file with the same name exists, it will be overwritten.
file_path
must provide the location where the file will be saved.
>>> d = 'RADkit'
>>> d
'RADkit'
>>> d | write_to_file('my_file_location.txt')
'File successfully saved at my_file_location.txt'
$ more my_radkit_file.txt
RADkit
>>> d = {'a':1, 'b':2, 'c':None}
>>> d | to_json | write_to_file('~/my_radkit_file.txt')
'File successfully saved at ~/my_radkit_file.txt'
$ more my_radkit_file.txt
{
"a": 1,
"b": 2,
"c": null
}
Note
Objects that need to be saved into files, should be serialized first with | to_json
.
append_to_file
| append_to_file([<file_path>])
Same as | write_to_file
but appends the multi-line string on the left to the end of file
if it exists. If the file does not exist, one will be created.
$ more my_radkit_file.txt
{
"a": 1,
"b": 2,
"c": null
}
>>> d = {'e':1, 'f':2, 'g':None}
>>> d | to_json | append_to_file('~/my_radkit_file.txt')
'File successfully saved at ~/my_radkit_file.txt'
$ more my_radkit_file.txt
{
"a": 1,
"b": 2,
"c": null
}{
"e": 1,
"f": 2,
"g": null
}
Note
Objects that need to be saved into files, should be serialized first with | to_json
.
Feature guides
To continue your exploration of RADKit, read on by moving to the feature guides: