Show routines

This chapter provides a tutorial on how to create a routine, or set of instructions, that results in a custom show command. It also provides a detailed description of all the classes and utility functions used to create custom show commands.

Before creating a show routine, the following is recommended:

  • Whenever you receive Data from the server, print it using:

    print(str(data))

    This makes it easier to visualize the data hierarchy.

  • While building the SchemaNode and populating the Data structure, do not focus on the layout of your show routine. Instead, use the output of the following:

    show <your-routine> | as json

    This JSON output contains all the information currently in your show routine, in the correct hierarchy. After the JSON output looks correct, then plan how to format it.

Several show routines are available in /opt/srlinux/python/virtual-env/lib/python3.11/dist-packages/srlinux/mgmt/cli/plugins/reports that you can use as examples.

Creating a show routine

To create a show routine, you perform the following high-level steps:

  1. Build a SchemaNode to describe the data’s data model the show routine will construct. Procedure: Building the SchemaNode
  2. Retrieve the state from the management server using state.server_data_store.get_data(<path>) Procedure: Retrieving the state from the management server
  3. Populate a Data object with all the data (keys/fields/…) of the show routine. Procedure: Populating a data object
  4. Add Formatter instances to determine how the data is formatted. Procedure: Adding formatter instances
  5. Implement the callback method to pass the Data structure to the output.print_data command. Procedure: Implementing the callback method
  6. Use streaming to optimize reports when working with large amounts of data. Procedure: Using streaming to optimize reports

After completing all steps, the following is an example of the output. As you perform each step in this section, you will see how this example is built.

--{ running }--[  ]--
# show my-report *
======================================
name       : interface-1
description: The first interface
admin-state: enable
--------------------------------------
Child-Id  Is-Cool
=================
24         no
42         yes
======================================
name       : interface-2
description: The second interface
admin-state: disable
--------------------------------------
Child-Id  Is-Cool
=================
1337       yes
======================================

Building the SchemaNode

Schema nodes describe a data model. Similar to the output of the tree command or the content of a YANG file, they indicate what lists, containers, keys, fields, and leaf-lists can be created.

  1. To build a SchemaNode, start with a FixedSchemaRoot()
    Then add your top-level list/container using FixedSchemaNode.add_child() as shown in the SchemaNode reference.
    from srlinux.schema import FixedSchemaRoot
    
    def _get_my_schema(self):
        root = FixedSchemaRoot()
        interface = root.add_child(
            'interface',
            key='name',
            fields=['description', 'admin-state']
        )
        child = interface.add_child(
            'child',
            key='Child-Id',
            fields=['Is-Cool']
        )
    

    The code above generates a data model for the following YANG model.

    list interface {
        key "name";
        leaf "description";
        leaf "admin-state";
        list child {
            key "Child-Id";
            leaf "Is-Cool";
        }
    }
    
  2. Ensure that the filter auto-completes all fields by passing the schema node into the add_command call when you install the plug-in. This ensures that the filter operator (|) can auto-complete all fields.
    class Plugin(CliPlugin):
        '''
    Adds a fancy show report named "my-report".
    '''
    
        def load(self, cli, **_kwargs):
            cli.show_mode.add_command(
                Syntax('my-report').add_unnamed_argument('name'),
                update_location=False,
                callback=self._print,
                schema=self._get_my_schema(),
            )
    

Retrieving the state from the management server

To retrieve the state, use build_path() to populate a path of the key you need to retrieve, and call get_data.

This returns a Data object pointing to the root of the data returned by the management server.

from srlinux.location import build_path

def _fetch_state(self, state, arguments):
    path = build_path('/interface[name={name}]/
subinterface[index=*]', name=arguments.get('name')) 

    return state.server_data_store.get_data(path, recursive=True)

Populating a data object

With the data from the management server and a data model, populate the Data object.

from srlinux.data import Data
from srlinux import strings

def _populate_data(self, server_data):
    result = Data(self._get_my_schema())

    for interface in server_data.interface.items():
        data = result.interface.create(interface.name)
        data.description = interface.description
        data.admin_state = interface.admin_state

        self._add_children(data, interface.subinterface)

    return result

def _add_children(self, data, server_data):
    # server_data is an instance of DataChildrenOfType

    for subinterface in server_data.items():
        child = data.child.create(subinterface.index)

        cool_ids = [42, 1337]
        is_cool = subinterface.index in cool_ids
        child.is_cool = strings.bool_to_yes_no(is_cool)

Adding formatter instances

To format the output, assign Formatter instances to the different Data objects. The type of Formatter determines whether the output is formatted using key value pairs, a grid-based table, or a custom format.

A list of all the built-in formatters is in the Formatters section.

from srlinux.data import Border, ColumnFormatter, Data, TagValueFormatter, Borders, Indent

def _set_formatters(self, data):
    data.set_formatter(
        '/interface',
        Border(TagValueFormatter(), Border.Above | Border.Below | Border.Between, 
        '='))

    data.set_formatter(
        '/interface/child',
        Indent(ColumnFormatter(ancestor_keys=False, borders=Borders.Header), indentation=2))

Implementing the callback method

The following shows how to implement the callback method that is used to complete the routine.

def _print(self, state, arguments, output, **_kwargs):
    server_data = self._fetch_state(state, arguments)
    result = self._populate_data(server_data)
    self._set_formatters(result)
    output.print_data(result)

Show routine code example

After you complete Steps 1 - 5, the show routine first shown in section Creating a show routine is now complete.

The following is an example of the complete show routine code.

from srlinux import strings
from srlinux.data import Border, ColumnFormatter, TagValueFormatter, Borders, Data, Indent
from srlinux.location import build_path
from srlinux.mgmt.cli import CliPlugin
from srlinux.schema import FixedSchemaRoot
from srlinux.syntax import Syntax

class Plugin(CliPlugin):
    def load(self, cli, **_kwargs):
        cli.show_mode.add_command(
            Syntax('my-report').add_unnamed_argument('name'),
            update_location=False,
            callback=self._print,
            schema=self._get_my_schema(),
        )

    def _print(self, state, arguments, output, **_kwargs):
        server_data = self._fetch_state(state, arguments)
        result = self._populate_data(server_data)
        self._set_formatters(result)
        output.print_data(result)

    def _get_my_schema(self):
        root = FixedSchemaRoot()
        interface = root.add_child(
            'interface',
            key='name',
            fields=['description', 'admin-state']
        )
        child = interface.add_child(
            'child',
            key='Child-Id',
            fields=['Is-Cool']
        )
        return root

    def _fetch_state(self, state, arguments):
        path = build_path('/interface[name={name}]/
subinterface[index=*]', name=arguments.get('name'))

        return state.server_data_store.get_data(path, recursive=True)
    def _populate_data(self, server_data):
        result = Data(self._get_my_schema())

        for interface in server_data.interface.items():
            data = result.interface.create(interface.name)
            data.description = interface.description
            data.admin_state = interface.admin_state

            self._add_children(data, interface.subinterface)

        return result

    def _add_children(self, data, server_data):
        # server_data is an instance of DataChildrenOfType

        for subinterface in server_data.items():
            child = data.child.create(subinterface.index)

            cool_ids = [42, 1337]
            is_cool = subinterface.index in cool_ids
            child.is_cool = strings.bool_to_yes_no(is_cool)

    def _set_formatters(self, data):
        data.set_formatter(
            '/interface',
            Border(TagValueFormatter(), Border.Above | Border.Below | Border.Between, '='))
        data.set_formatter(
            '/interface/child',
            Indent(ColumnFormatter(ancestor_keys=False, borders=Borders.Header), indentation=2))

Using streaming to optimize reports

The previous steps detail how to obtain data, and then separately print a report. With streaming, data is retrieved and begins printing immediately. This is useful for reports that have large amounts of data (for example, route tables) because printing begins immediately instead of waiting for the entire data collection to complete.

Perform the following to implement streaming.

  1. Enter state.server_data_store.stream_data(<path>) instead of state.server_data_store.get_data(<path>) to retrieve server data.
    def _fetch_state(self, state, arguments):
        path = build_path('/interface[name={name}]/subinterface[index=*]', 
        name=arguments.get('name'))
        return state.server_data_store.stream_data(path, recursive=True)
    
  2. When constructing data, flush the data using flush_fields() and flush_children(). If no data is flushed, it displays as though streaming was not implemented. Implement flushing as soon as you know that a node is finished (but not sooner).
            data.synchronizer.flush_fields(data)
    
            for interface in server_data.interface.items():
                data = data_root.interface.create(interface.name)
                data.description = interface.description
                data.admin_state = interface.admin_state
                data.synchronizer.flush_fields(data)
                data_root.synchronizer.flush_children(data_root.interface)
    
    Note: You cannot change fields after a flush_fields() is called, and you cannot create new child nodes for the list you called flush_children().
  3. Enter output.stream_data instead of output.print_data. Because this is a context manager python class, it can be used as `with output.stream_data(result):`. When the `with` block exits, flush_fields() and flush_children() are called on every node to ensure everything prints at the end.
    def _print(self, state, arguments, output, **_kwargs):
        result = Data(arguments.schema)
        self._set_formatters(result)
        with output.steam_data(result)
        self._populate_data_V4(result)
    
  4. Ensure formatted output is aligned. If needed, the column width can be explicitly passed to the formatter.
    Pass width of columns to formatter ::
        ColumnFormatter(widths=[Percentage(10), 6, 10, Width(min_width=8,
        percent=20)])
    

The following is an example of the complete show routine code using streaming.

from srlinux import strings
from srlinux.data import Border, ColumnFormatter, TagValueFormatter, Borders, Data, Indent
from srlinux.location import build_path
from srlinux.mgmt.cli import CliPlugin
from srlinux.schema import FixedSchemaRoot
from srlinux.syntax import Syntax

class Plugin(CliPlugin):
    def load(self, cli, **_kwargs):
        cli.show_mode.add_command(
            Syntax('my-report').add_unnamed_argument('name'),
            update_location=False,
            callback=self._print,
            schema=self._get_my_schema(),
        )

    def _print(self, state, arguments, output, **_kwargs):
        result = Data(arguments.schema)
        self._set_formatters(result)
        with output.steam_data(result)
        self._populate_data(result, state, arguments)

    def _get_my_schema(self):
        root = FixedSchemaRoot()
        interface = root.add_child(
            'interface',
            key='name',
            fields=['description', 'admin-state']
        )
        return root

    def _fetch_state(self, state, arguments):
        path = build_path('/interface[name={name}]/
        subinterface[index=*]', name=arguments.get('name'))
        return state.server_data_store.stream_data(path, recursive=True)

    def _populate_data(self, data_root, state, arguments):
        server_data = self._fetch_state(state, arguments)
        data_root.synchronizer.flush_fields(data_root)
        for interface in server_data.interface.items():
            data = data_root.interface.create(interface.name)
            data.description = interface.description
            data.admin_state = interface.admin_state
            data.synchronizer.flush_fields(data)
            data_root.synchronizer.flush_children(data_root.interface)


    def _set_formatters(self, data):
        data.set_formatter(
            '/interface',
            Border(TagValueFormatter(), Border.Above | Border.Below | Border.Between, '='))