Skip to content

Commit

Permalink
[sonic-package-manager] add support for multiple CLI plugin files (#2753
Browse files Browse the repository at this point in the history
)

#### What I did

Use case for some application extensions we develop is to have multiple features as part of a single container. Therefore we want to have ability to install multiple CLI plugins from a single extension.

#### How I did it

Allowed to specify few CLI plugins in the manifest.

#### How to verify it

Build extension with multiple CLI plugins and install. Verified all plugins are installed.
  • Loading branch information
stepanblyschak authored Apr 30, 2023
1 parent b38fcfd commit 522c3a9
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 18 deletions.
25 changes: 14 additions & 11 deletions sonic_package_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,18 @@ def get_cli_plugin_directory(command: str) -> str:
return plugins_pkg_path


def get_cli_plugin_path(package: Package, command: str) -> str:
def get_cli_plugin_path(package: Package, index: int, command: str) -> str:
""" Returns a path where to put CLI plugin code.
Args:
package: Package to generate this path for.
index: Index of a cli plugin
command: SONiC command: "show"/"config"/"clear".
Returns:
Path generated for this package.
"""

plugin_module_file = package.name + '.py'
plugin_module_file = f'{package.name}_{index}.py'
return os.path.join(get_cli_plugin_directory(command), plugin_module_file)


Expand Down Expand Up @@ -978,19 +979,21 @@ def _uninstall_cli_plugins(self, package: Package):
self._uninstall_cli_plugin(package, command)

def _install_cli_plugin(self, package: Package, command: str):
image_plugin_path = package.manifest['cli'][command]
if not image_plugin_path:
image_plugins = package.manifest['cli'][command]
if not image_plugins:
return
host_plugin_path = get_cli_plugin_path(package, command)
self.docker.extract(package.entry.image_id, image_plugin_path, host_plugin_path)
for index, image_plugin_path in enumerate(image_plugins):
host_plugin_path = get_cli_plugin_path(package, index, command)
self.docker.extract(package.entry.image_id, image_plugin_path, host_plugin_path)

def _uninstall_cli_plugin(self, package: Package, command: str):
image_plugin_path = package.manifest['cli'][command]
if not image_plugin_path:
image_plugins = package.manifest['cli'][command]
if not image_plugins:
return
host_plugin_path = get_cli_plugin_path(package, command)
if os.path.exists(host_plugin_path):
os.remove(host_plugin_path)
for index, _ in enumerate(image_plugins):
host_plugin_path = get_cli_plugin_path(package, index, command)
if os.path.exists(host_plugin_path):
os.remove(host_plugin_path)

@staticmethod
def get_manager() -> 'PackageManager':
Expand Down
26 changes: 23 additions & 3 deletions sonic_package_manager/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ def marshal(self, value):
def unmarshal(self, value):
return value

@dataclass
class ListMarshaller(Marshaller):
""" Returns a list. In case input is of other type it returns a list containing that single value. """

type: type

def marshal(self, value):
if isinstance(value, list):
for item in value:
if not isinstance(item, self.type):
raise ManifestError(f'{value} has items not of type {self.type.__name__}')
return value
elif isinstance(value, self.type):
return [value]
else:
raise ManifestError(f'{value} is not of type {self.type.__name__}')

def unmarshal(self, value):
return value

@dataclass
class ManifestNode(Marshaller, ABC):
"""
Expand Down Expand Up @@ -207,9 +227,9 @@ def unmarshal(self, value):
])),
ManifestRoot('cli', [
ManifestField('mandatory', DefaultMarshaller(bool), False),
ManifestField('show', DefaultMarshaller(str), ''),
ManifestField('config', DefaultMarshaller(str), ''),
ManifestField('clear', DefaultMarshaller(str), ''),
ManifestField('show', ListMarshaller(str), []),
ManifestField('config', ListMarshaller(str), []),
ManifestField('clear', ListMarshaller(str), []),
ManifestField('auto-generate-show', DefaultMarshaller(bool), False),
ManifestField('auto-generate-config', DefaultMarshaller(bool), False),
])
Expand Down
37 changes: 33 additions & 4 deletions tests/sonic_package_manager/test_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env python

import re
from unittest.mock import Mock, call
from unittest.mock import Mock, call, patch

import pytest

import sonic_package_manager
from sonic_package_manager.errors import *
from sonic_package_manager.version import Version

Expand Down Expand Up @@ -161,9 +162,37 @@ def test_installation_base_os_constraint_satisfied(package_manager, fake_metadat
def test_installation_cli_plugin(package_manager, fake_metadata_resolver, anything):
manifest = fake_metadata_resolver.metadata_store['Azure/docker-test']['1.6.0']['manifest']
manifest['cli']= {'show': '/cli/plugin.py'}
package_manager._install_cli_plugins = Mock()
package_manager.install('test-package')
package_manager._install_cli_plugins.assert_called_once_with(anything)
with patch('sonic_package_manager.manager.get_cli_plugin_directory') as get_dir_mock:
get_dir_mock.return_value = '/'
package_manager.install('test-package')
package_manager.docker.extract.assert_called_once_with(anything, '/cli/plugin.py', '/test-package_0.py')


def test_installation_multiple_cli_plugin(package_manager, fake_metadata_resolver, mock_feature_registry, anything):
manifest = fake_metadata_resolver.metadata_store['Azure/docker-test']['1.6.0']['manifest']
manifest['cli']= {'show': ['/cli/plugin.py', '/cli/plugin2.py']}
with patch('sonic_package_manager.manager.get_cli_plugin_directory') as get_dir_mock, \
patch('os.remove') as remove_mock, \
patch('os.path.exists') as path_exists_mock:
get_dir_mock.return_value = '/'
package_manager.install('test-package')
package_manager.docker.extract.assert_has_calls(
[
call(anything, '/cli/plugin.py', '/test-package_0.py'),
call(anything, '/cli/plugin2.py', '/test-package_1.py'),
],
any_order=True,
)

package_manager._set_feature_state = Mock()
package_manager.uninstall('test-package', force=True)
remove_mock.assert_has_calls(
[
call('/test-package_0.py'),
call('/test-package_1.py'),
],
any_order=True,
)


def test_installation_cli_plugin_skipped(package_manager, fake_metadata_resolver, anything):
Expand Down

0 comments on commit 522c3a9

Please sign in to comment.