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:
- Build a SchemaNode to describe the data’s data model the show routine will construct. Procedure: Building the SchemaNode
- Retrieve the state from the management server using state.server_data_store.get_data(<path>) Procedure: Retrieving the state from the management server
- Populate a Data object with all the data (keys/fields/…) of the show routine. Procedure: Populating a data object
- Add Formatter instances to determine how the data is formatted. Procedure: Adding formatter instances
- Implement the callback method to pass the Data structure to the output.print_data command. Procedure: Implementing the callback method
- 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.
-
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"; } }
-
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.
-
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)
- 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(). -
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)
- 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, '='))