2. 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:

  1. Whenever you receive Data from the server, print it using:
    print(str(data))
    This makes it easier to visualize the data hierarchy.
  2. 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. Once the JSON output looks correct, then plan how to format it.

2.1. Create 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.
  2. Retrieve the state from the management server using state.server_data_store.get_data(<path>)
  3. Populate a Data object with all the data (keys/fields/…) of the show routine.
  4. Add Formatter instances to determine how the data will be formatted.
  5. Implement the callback method to pass the Data structure to the output.print_data command.
  6. Use streaming to optimize reports when working with large amounts of data.

The following is an output example once all steps are completed. As you perform the steps in this section, you will be able to see how this example is built.

======================================
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
======================================

2.1.1. Step 1: Build 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.

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.

For example:

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";
    }
}

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. For example:

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

2.1.2. Step 2: Retrieve 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)

2.1.3. Step 3: Populate 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)

2.1.4. Step 4: Add 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, as a grid-based table, or using a custom format.

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), indenta
tion=2))

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

2.1.5. Step 5: Implement the callback method

The following example shows how to implement the callback method which can then be invoked 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)

2.1.6. Show routine code example

Once you have completed Steps 1 - 5 (in sections  2.1.1 -  2.1.5), the show routine first shown in section  2.1 Create 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('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), ind
entation=2))

2.1.7. Step 6: Use 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
    For example:
     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).
    For example:
            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 that 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 it 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.
    For example:
        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('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
 , '='))