Skip to content

Commit

Permalink
[cli] Dynamic cli extension via plugins (sonic-net#1186)
Browse files Browse the repository at this point in the history
Added a mechanism to extend command line interface via plugins.
For every program - show, config, clear a new python package called 'plugins' is added. This package is used as a namespace for all plugins python modules. As an example mlnx.py is moved to the plugins package.

In the fututre, this mechanism will be used by Application Extension infrastructure.

Signed-off-by: Stepan Blyschak <stepanb@nvidia.com>
  • Loading branch information
stepanblyschak committed Mar 31, 2021
1 parent 6b51bcd commit 9a2872d
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 14 deletions.
12 changes: 11 additions & 1 deletion clear/main.py
Expand Up @@ -5,6 +5,10 @@

import click

from utilities_common import util_base

from . import plugins


# This is from the aliases example:
# https://github.com/pallets/click/blob/57c6f09611fc47ca80db0bd010f05998b3c0aa95/examples/aliases/aliases.py
Expand Down Expand Up @@ -120,7 +124,6 @@ def cli():
"""SONiC command line - 'Clear' command"""
pass


#
# 'ip' group ###
#
Expand Down Expand Up @@ -446,5 +449,12 @@ def translations():
cmd = "natclear -t"
run_command(cmd)


# Load plugins and register them
helper = util_base.UtilHelper()
for plugin in helper.load_plugins(plugins):
helper.register_plugin(plugin, cli)


if __name__ == '__main__':
cli()
Empty file added clear/plugins/__init__.py
Empty file.
13 changes: 9 additions & 4 deletions config/main.py
Expand Up @@ -16,6 +16,7 @@
from portconfig import get_child_ports
from sonic_py_common import device_info, multi_asic
from sonic_py_common.interface import get_interface_table_name, get_port_table_name
from utilities_common import util_base
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, SonicDBConfig
from utilities_common.db import Db
from utilities_common.intf_filter import parse_interface_in_filter
Expand All @@ -28,11 +29,11 @@
from . import feature
from . import kdump
from . import kube
from . import mlnx
from . import muxcable
from . import nat
from . import vlan
from . import vxlan
from . import plugins
from .config_mgmt import ConfigMgmtDPB

# mock masic APIs for unit test
Expand Down Expand Up @@ -849,9 +850,6 @@ def config(ctx):
except (KeyError, TypeError):
raise click.Abort()

if asic_type == 'mellanox':
platform.add_command(mlnx.mlnx)

# Load the global config file database_global.json once.
num_asic = multi_asic.get_num_asics()
if num_asic > 1:
Expand Down Expand Up @@ -4415,5 +4413,12 @@ def delete(ctx):
sflow_tbl['global'].pop('agent_id')
config_db.set_entry('SFLOW', 'global', sflow_tbl['global'])


# Load plugins and register them
helper = util_base.UtilHelper()
for plugin in helper.load_plugins(plugins):
helper.register_plugin(plugin, config)


if __name__ == '__main__':
config()
Empty file added config/plugins/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions config/mlnx.py → config/plugins/mlnx.py
Expand Up @@ -11,6 +11,7 @@

import click
from sonic_py_common import logger
from sonic_py_common import device_info
import utilities_common.cli as clicommon
except ImportError as e:
raise ImportError("%s - required module not found" % str(e))
Expand Down Expand Up @@ -229,5 +230,10 @@ def sdk_sniffer_disable():
# pass


def register(cli):
version_info = device_info.get_sonic_version_info()
if (version_info and version_info.get('asic_type') == 'mellanox'):
cli.commands['platform'].add_command(mlnx)

if __name__ == '__main__':
sniffer()
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -22,7 +22,9 @@
packages=[
'acl_loader',
'clear',
'clear.plugins',
'config',
'config.plugins',
'connect',
'consutil',
'counterpoll',
Expand All @@ -42,6 +44,7 @@
'pddf_ledutil',
'show',
'show.interfaces',
'show.plugins',
'sonic_installer',
'sonic_installer.bootloader',
'tests',
Expand Down
10 changes: 9 additions & 1 deletion show/main.py 100644 → 100755
Expand Up @@ -11,6 +11,7 @@
from sonic_py_common import device_info, multi_asic
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector
from tabulate import tabulate
from utilities_common import util_base
from utilities_common.db import Db

from . import acl
Expand All @@ -23,7 +24,6 @@
from . import interfaces
from . import kdump
from . import kube
from . import mlnx
from . import muxcable
from . import nat
from . import platform
Expand All @@ -35,6 +35,7 @@
from . import vxlan
from . import system_health
from . import warm_restart
from . import plugins


# Global Variables
Expand Down Expand Up @@ -1413,5 +1414,12 @@ def ztp(status, verbose):
cmd = cmd + " --verbose"
run_command(cmd, display_cmd=verbose)


# Load plugins and register them
helper = util_base.UtilHelper()
for plugin in helper.load_plugins(plugins):
helper.register_plugin(plugin, cli)


if __name__ == '__main__':
cli()
6 changes: 0 additions & 6 deletions show/platform.py
Expand Up @@ -33,12 +33,6 @@ def platform():
pass


version_info = device_info.get_sonic_version_info()
if (version_info and version_info.get('asic_type') == 'mellanox'):
from . import mlnx
platform.add_command(mlnx.mlnx)


# 'summary' subcommand ("show platform summary")
@platform.command()
@click.option('--json', is_flag=True, help="Output in JSON format")
Expand Down
Empty file added show/plugins/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions show/mlnx.py → show/plugins/mlnx.py
Expand Up @@ -10,6 +10,7 @@
import subprocess
import click
import xml.etree.ElementTree as ET
from sonic_py_common import device_info
except ImportError as e:
raise ImportError("%s - required module not found" % str(e))

Expand Down Expand Up @@ -137,3 +138,8 @@ def issu_status():

click.echo('ISSU is enabled' if res else 'ISSU is disabled')


def register(cli):
version_info = device_info.get_sonic_version_info()
if (version_info and version_info.get('asic_type') == 'mellanox'):
cli.commands['platform'].add_command(mlnx)
40 changes: 38 additions & 2 deletions utilities_common/util_base.py
@@ -1,17 +1,51 @@

import os
import sonic_platform
import pkgutil
import importlib

from sonic_py_common import logger

# Constants ====================================================================
PDDF_SUPPORT_FILE = '/usr/share/sonic/platform/pddf_support'

# Helper classs

log = logger.Logger()


class UtilHelper(object):
def __init__(self):
pass

def load_plugins(self, plugins_namespace):
""" Discover and load CLI plugins. Yield a plugin module. """

def iter_namespace(ns_pkg):
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")

for _, module_name, ispkg in iter_namespace(plugins_namespace):
if ispkg:
continue
log.log_debug('importing plugin: {}'.format(module_name))
try:
module = importlib.import_module(module_name)
except Exception as err:
log.log_error('failed to import plugin {}: {}'.format(module_name, err),
also_print_to_console=True)
continue

yield module

def register_plugin(self, plugin, root_command):
""" Register plugin in top-level command root_command. """

name = plugin.__name__
log.log_debug('registering plugin: {}'.format(name))
try:
plugin.register(root_command)
except Exception as err:
log.log_error('failed to import plugin {}: {}'.format(name, err),
also_print_to_console=True)

# try get information from platform API and return a default value if caught NotImplementedError
def try_get(self, callback, default=None):
"""
Expand All @@ -35,6 +69,7 @@ def load_platform_chassis(self):

# Load 2.0 platform API chassis class
try:
import sonic_platform
chassis = sonic_platform.platform.Platform().get_chassis()
except Exception as e:
raise Exception("Failed to load chassis due to {}".format(repr(e)))
Expand All @@ -47,3 +82,4 @@ def check_pddf_mode(self):
return True
else:
return False

0 comments on commit 9a2872d

Please sign in to comment.