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 (with pip 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 creating profile.py, or

  • by 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 or certificate_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 more Service 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. Each Device is tied to a specific Service which is itself tied to your Client, 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 a radkit_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, the data 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 the Object parameters in the output above. The most important parameter is name, which is the display name as well as the user-facing unique identifier for that device on the Service side. The other two are the identity and serial parameters, which are identical to those of the device’s parent radkit_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, if terminal_config is True, 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 using Device.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 of name values or Device 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 more Device 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

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: