From f5e5a567f7e6f87eb3de81d29d988e042940e49a Mon Sep 17 00:00:00 2001 From: Stepan Blyshchak <38952541+stepanblyschak@users.noreply.github.com> Date: Wed, 24 Nov 2021 14:55:01 +0200 Subject: [PATCH] [sonic-package-manager] support sonic-cli-gen and packages with YANG model (#1650) - What I did This PR brings in support for packages with YANG models and CLI auto generation capabilities for 3rd party packages. - How I did it Packages can set two new flags in manifest - "auto-generate-show" and "auto-generate-config" in addition to YANG module recorded in package image label "com.azure.sonic.yang-module". - How to verify it Build and run. Prepare some package with YANG model and test CLI is generated for it. Signed-off-by: Stepan Blyshchak Co-authored-by: Vadym Hlushko --- config/config_mgmt.py | 110 +++++++++-- sonic_package_manager/main.py | 4 +- sonic_package_manager/manager.py | 21 ++- sonic_package_manager/manifest.py | 4 +- sonic_package_manager/metadata.py | 6 +- .../service_creator/creator.py | 172 ++++++++++++++++-- .../service_creator/feature.py | 12 -- tests/sonic_package_manager/conftest.py | 34 +++- .../test_service_creator.py | 113 +++++++++--- 9 files changed, 380 insertions(+), 96 deletions(-) diff --git a/config/config_mgmt.py b/config/config_mgmt.py index 9b2021bef0c5..bbed677fa634 100644 --- a/config/config_mgmt.py +++ b/config/config_mgmt.py @@ -2,8 +2,13 @@ config_mgmt.py provides classes for configuration validation and for Dynamic Port Breakout. ''' + +import os import re +import shutil import syslog +import tempfile +import yang as ly from json import load from sys import flags from time import sleep as tsleep @@ -46,27 +51,14 @@ def __init__(self, source="configDB", debug=False, allowTablesWithoutYang=True): try: self.configdbJsonIn = None self.configdbJsonOut = None + self.source = source self.allowTablesWithoutYang = allowTablesWithoutYang # logging vars self.SYSLOG_IDENTIFIER = "ConfigMgmt" self.DEBUG = debug - self.sy = sonic_yang.SonicYang(YANG_DIR, debug=debug) - # load yang models - self.sy.loadYangModel() - # load jIn from config DB or from config DB json file. - if source.lower() == 'configdb': - self.readConfigDB() - # treat any other source as file input - else: - self.readConfigDBJson(source) - # this will crop config, xlate and load. - self.sy.loadData(self.configdbJsonIn) - - # Raise if tables without YANG models are not allowed but exist. - if not allowTablesWithoutYang and len(self.sy.tablesWithOutYang): - raise Exception('Config has tables without YANG models') + self.__init_sonic_yang() except Exception as e: self.sysLog(doPrint=True, logLevel=syslog.LOG_ERR, msg=str(e)) @@ -74,6 +66,23 @@ def __init__(self, source="configDB", debug=False, allowTablesWithoutYang=True): return + def __init_sonic_yang(self): + self.sy = sonic_yang.SonicYang(YANG_DIR, debug=self.DEBUG) + # load yang models + self.sy.loadYangModel() + # load jIn from config DB or from config DB json file. + if self.source.lower() == 'configdb': + self.readConfigDB() + # treat any other source as file input + else: + self.readConfigDBJson(self.source) + # this will crop config, xlate and load. + self.sy.loadData(self.configdbJsonIn) + + # Raise if tables without YANG models are not allowed but exist. + if not self.allowTablesWithoutYang and len(self.sy.tablesWithOutYang): + raise Exception('Config has tables without YANG models') + def __del__(self): pass @@ -213,6 +222,69 @@ def writeConfigDB(self, jDiff): return + def add_module(self, yang_module_str): + """ + Validate and add new YANG module to the system. + + Parameters: + yang_module_str (str): YANG module in string representation. + + Returns: + None + """ + + module_name = self.get_module_name(yang_module_str) + module_path = os.path.join(YANG_DIR, '{}.yang'.format(module_name)) + if os.path.exists(module_path): + raise Exception('{} already exists'.format(module_name)) + with open(module_path, 'w') as module_file: + module_file.write(yang_module_str) + try: + self.__init_sonic_yang() + except Exception: + os.remove(module_path) + raise + + def remove_module(self, module_name): + """ + Remove YANG module from the system and validate. + + Parameters: + module_name (str): YANG module name. + + Returns: + None + """ + + module_path = os.path.join(YANG_DIR, '{}.yang'.format(module_name)) + if not os.path.exists(module_path): + return + temp = tempfile.NamedTemporaryFile(delete=False) + try: + shutil.move(module_path, temp.name) + self.__init_sonic_yang() + except Exception: + shutil.move(temp.name, module_path) + raise + + @staticmethod + def get_module_name(yang_module_str): + """ + Read yangs module name from yang_module_str + + Parameters: + yang_module_str(str): YANG module string. + + Returns: + str: Module name + """ + + # Instantiate new context since parse_module_mem() loads the module into context. + sy = sonic_yang.SonicYang(YANG_DIR) + module = sy.ctx.parse_module_mem(yang_module_str, ly.LYS_IN_YANG) + return module.name() + + # End of Class ConfigMgmt class ConfigMgmtDPB(ConfigMgmt): @@ -417,8 +489,8 @@ def _deletePorts(self, ports=list(), force=False): deps.extend(dep) # No further action with no force and deps exist - if force == False and deps: - return configToLoad, deps, False; + if not force and deps: + return configToLoad, deps, False # delets all deps, No topological sort is needed as of now, if deletion # of deps fails, return immediately @@ -436,8 +508,8 @@ def _deletePorts(self, ports=list(), force=False): self.sy.deleteNode(str(xPathPort)) # Let`s Validate the tree now - if self.validateConfigData()==False: - return configToLoad, deps, False; + if not self.validateConfigData(): + return configToLoad, deps, False # All great if we are here, Lets get the diff self.configdbJsonOut = self.sy.getData() diff --git a/sonic_package_manager/main.py b/sonic_package_manager/main.py index dfb88bd58d94..8a0aabb9016d 100644 --- a/sonic_package_manager/main.py +++ b/sonic_package_manager/main.py @@ -414,10 +414,11 @@ def reset(ctx, name, force, yes, skip_host_plugins): @cli.command() @add_options(PACKAGE_COMMON_OPERATION_OPTIONS) +@click.option('--keep-config', is_flag=True, help='Keep features configuration in CONFIG DB.') @click.argument('name') @click.pass_context @root_privileges_required -def uninstall(ctx, name, force, yes): +def uninstall(ctx, name, force, yes, keep_config): """ Uninstall package. """ manager: PackageManager = ctx.obj @@ -428,6 +429,7 @@ def uninstall(ctx, name, force, yes): uninstall_opts = { 'force': force, + 'keep_config': keep_config, } try: diff --git a/sonic_package_manager/manager.py b/sonic_package_manager/manager.py index f7bbd1afdd44..7dc1ae773a20 100644 --- a/sonic_package_manager/manager.py +++ b/sonic_package_manager/manager.py @@ -10,8 +10,11 @@ import docker import filelock +from config import config_mgmt from sonic_py_common import device_info +from sonic_cli_gen.generator import CliGenerator + from sonic_package_manager import utils from sonic_package_manager.constraint import ( VersionConstraint, @@ -45,7 +48,10 @@ run_command ) from sonic_package_manager.service_creator.feature import FeatureRegistry -from sonic_package_manager.service_creator.sonic_db import SonicDB +from sonic_package_manager.service_creator.sonic_db import ( + INIT_CFG_JSON, + SonicDB +) from sonic_package_manager.service_creator.utils import in_chroot from sonic_package_manager.source import ( PackageSource, @@ -435,13 +441,16 @@ def install_from_source(self, @under_lock @opt_check - def uninstall(self, name: str, force=False): + def uninstall(self, name: str, + force: bool = False, + keep_config: bool = False): """ Uninstall SONiC Package referenced by name. The uninstallation can be forced if force argument is True. Args: name: SONiC Package name. force: Force the installation. + keep_config: Keep feature configuration in databases. Raises: PackageManagerError """ @@ -482,7 +491,7 @@ def uninstall(self, name: str, force=False): self._systemctl_action(package, 'stop') self._systemctl_action(package, 'disable') self._uninstall_cli_plugins(package) - self.service_creator.remove(package) + self.service_creator.remove(package, keep_config=keep_config) self.service_creator.generate_shutdown_sequence_files( self._get_installed_packages_except(package) ) @@ -1000,9 +1009,13 @@ def get_manager() -> 'PackageManager': docker_api = DockerApi(docker.from_env(), ProgressManager()) registry_resolver = RegistryResolver() metadata_resolver = MetadataResolver(docker_api, registry_resolver) + cfg_mgmt = config_mgmt.ConfigMgmt(source=INIT_CFG_JSON) + cli_generator = CliGenerator(log) feature_registry = FeatureRegistry(SonicDB) service_creator = ServiceCreator(feature_registry, - SonicDB) + SonicDB, + cli_generator, + cfg_mgmt) return PackageManager(docker_api, registry_resolver, diff --git a/sonic_package_manager/manifest.py b/sonic_package_manager/manifest.py index e127fbb5384b..2d9f3514e784 100644 --- a/sonic_package_manager/manifest.py +++ b/sonic_package_manager/manifest.py @@ -205,7 +205,9 @@ def unmarshal(self, value): ManifestField('mandatory', DefaultMarshaller(bool), False), ManifestField('show', DefaultMarshaller(str), ''), ManifestField('config', DefaultMarshaller(str), ''), - ManifestField('clear', DefaultMarshaller(str), '') + ManifestField('clear', DefaultMarshaller(str), ''), + ManifestField('auto-generate-show', DefaultMarshaller(bool), False), + ManifestField('auto-generate-config', DefaultMarshaller(bool), False), ]) ]) diff --git a/sonic_package_manager/metadata.py b/sonic_package_manager/metadata.py index 1fc2cf3975eb..9cfa25e94ea3 100644 --- a/sonic_package_manager/metadata.py +++ b/sonic_package_manager/metadata.py @@ -4,7 +4,7 @@ import json import tarfile -from typing import Dict +from typing import Dict, Optional from sonic_package_manager import utils from sonic_package_manager.errors import MetadataError @@ -54,6 +54,7 @@ class Metadata: manifest: Manifest components: Dict[str, Version] = field(default_factory=dict) + yang_module_str: Optional[str] = None class MetadataResolver: @@ -163,5 +164,6 @@ def from_labels(cls, labels: Dict[str, str]) -> Metadata: except ValueError as err: raise MetadataError(f'Failed to parse component version: {err}') - return Metadata(Manifest.marshal(manifest_dict), components) + yang_module_str = sonic_metadata.get('yang-module') + return Metadata(Manifest.marshal(manifest_dict), components, yang_module_str) diff --git a/sonic_package_manager/service_creator/creator.py b/sonic_package_manager/service_creator/creator.py index 6839ffa453f1..bdc0b434b909 100644 --- a/sonic_package_manager/service_creator/creator.py +++ b/sonic_package_manager/service_creator/creator.py @@ -8,17 +8,24 @@ from typing import Dict, Type import jinja2 as jinja2 +from config.config_mgmt import ConfigMgmt from prettyprinter import pformat from toposort import toposort_flatten, CircularDependencyError +from config.config_mgmt import sonic_cfggen +from sonic_cli_gen.generator import CliGenerator from sonic_package_manager import utils from sonic_package_manager.logger import log from sonic_package_manager.package import Package -from sonic_package_manager.service_creator import ETC_SONIC_PATH +from sonic_package_manager.service_creator import ( + ETC_SONIC_PATH, + SONIC_CLI_COMMANDS, +) from sonic_package_manager.service_creator.feature import FeatureRegistry from sonic_package_manager.service_creator.sonic_db import SonicDB from sonic_package_manager.service_creator.utils import in_chroot + SERVICE_FILE_TEMPLATE = 'sonic.service.j2' TIMER_UNIT_TEMPLATE = 'timer.unit.j2' @@ -116,16 +123,22 @@ class ServiceCreator: def __init__(self, feature_registry: FeatureRegistry, - sonic_db: Type[SonicDB]): + sonic_db: Type[SonicDB], + cli_gen: CliGenerator, + cfg_mgmt: ConfigMgmt): """ Initialize ServiceCreator with: Args: feature_registry: FeatureRegistry object. sonic_db: SonicDB interface. + cli_gen: CliGenerator instance. + cfg_mgmt: ConfigMgmt instance. """ self.feature_registry = feature_registry self.sonic_db = sonic_db + self.cli_gen = cli_gen + self.cfg_mgmt = cfg_mgmt def create(self, package: Package, @@ -151,25 +164,27 @@ def create(self, self.generate_systemd_service(package) self.generate_dump_script(package) self.generate_service_reconciliation_file(package) - + self.install_yang_module(package) self.set_initial_config(package) + self.install_autogen_cli_all(package) self._post_operation_hook() if register_feature: - self.feature_registry.register(package.manifest, - state, owner) + self.feature_registry.register(package.manifest, state, owner) except (Exception, KeyboardInterrupt): - self.remove(package, register_feature) + self.remove(package, deregister_feature=register_feature) raise def remove(self, package: Package, - deregister_feature: bool = True): + deregister_feature: bool = True, + keep_config: bool = False): """ Uninstall SONiC service provided by the package. Args: package: Package object to uninstall. deregister_feature: Wether to deregister this package from FEATURE table. + keep_config: Whether to remove package configuration. Returns: None @@ -183,11 +198,16 @@ def remove(self, remove_if_exists(os.path.join(DEBUG_DUMP_SCRIPT_LOCATION, f'{name}')) remove_if_exists(os.path.join(ETC_SONIC_PATH, f'{name}_reconcile')) self.update_dependent_list_file(package, remove=True) + + if deregister_feature and not keep_config: + self.remove_config(package) + + self.uninstall_autogen_cli_all(package) + self.uninstall_yang_module(package) self._post_operation_hook() if deregister_feature: self.feature_registry.deregister(package.manifest['service']['name']) - self.remove_config(package) def generate_container_mgmt(self, package: Package): """ Generates container management script under /usr/bin/.sh for package. @@ -308,7 +328,6 @@ def update_dependent_list_file(self, package: Package, remove=False): remove: True if update for removal process. Returns: None. - """ name = package.manifest['service']['name'] @@ -476,13 +495,11 @@ def set_initial_config(self, package): cfg = conn.get_config() new_cfg = init_cfg.copy() utils.deep_update(new_cfg, cfg) + self.validate_config(new_cfg) conn.mod_config(new_cfg) def remove_config(self, package): - """ Remove configuration based on init-cfg tables, so having - init-cfg even with tables without keys might be a good idea. - TODO: init-cfg should be validated with yang model - TODO: remove config from tables known to yang model + """ Remove configuration based on package YANG module. Args: package: Package object remove initial configuration for. @@ -490,14 +507,131 @@ def remove_config(self, package): None """ - init_cfg = package.manifest['package']['init-cfg'] - if not init_cfg: + if not package.metadata.yang_module_str: return - for conn in self.sonic_db.get_connectors(): - for table in init_cfg: - for key in init_cfg[table]: - conn.set_entry(table, key, None) + module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str) + for tablename, module in self.cfg_mgmt.sy.confDbYangMap.items(): + if module.get('module') != module_name: + continue + + for conn in self.sonic_db.get_connectors(): + keys = conn.get_table(tablename).keys() + for key in keys: + conn.set_entry(tablename, key, None) + + def validate_config(self, config): + """ Validate configuration through YANG. + + Args: + config: Config DB data. + Returns: + None. + Raises: + Exception: if config does not pass YANG validation. + """ + + config = sonic_cfggen.FormatConverter.to_serialized(config) + log.debug(f'validating configuration {pformat(config)}') + # This will raise exception if configuration is not valid. + # NOTE: loadData() modifies the state of ConfigMgmt instance. + # This is not desired for configuration validation only purpose. + # Although the config loaded into ConfigMgmt instance is not + # interesting in this application so we don't care. + self.cfg_mgmt.loadData(config) + + def install_yang_module(self, package: Package): + """ Install package's yang module in the system. + + Args: + package: Package object. + Returns: + None + """ + + if not package.metadata.yang_module_str: + return + + self.cfg_mgmt.add_module(package.metadata.yang_module_str) + + def uninstall_yang_module(self, package: Package): + """ Uninstall package's yang module in the system. + + Args: + package: Package object. + Returns: + None + """ + + if not package.metadata.yang_module_str: + return + + module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str) + self.cfg_mgmt.remove_module(module_name) + + def install_autogen_cli_all(self, package: Package): + """ Install autogenerated CLI plugins for package. + + Args: + package: Package + Returns: + None + """ + + for command in SONIC_CLI_COMMANDS: + self.install_autogen_cli(package, command) + + def uninstall_autogen_cli_all(self, package: Package): + """ Remove autogenerated CLI plugins for package. + + Args: + package: Package + Returns: + None + """ + + for command in SONIC_CLI_COMMANDS: + self.uninstall_autogen_cli(package, command) + + def install_autogen_cli(self, package: Package, command: str): + """ Install autogenerated CLI plugins for package for particular command. + + Args: + package: Package. + command: Name of command to generate CLI for. + Returns: + None + """ + + if package.metadata.yang_module_str is None: + return + if f'auto-generate-{command}' not in package.manifest['cli']: + return + if not package.manifest['cli'][f'auto-generate-{command}']: + return + module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str) + self.cli_gen.generate_cli_plugin(command, module_name) + log.debug(f'{command} command line interface autogenerated for {module_name}') + + def uninstall_autogen_cli(self, package: Package, command: str): + """ Uninstall autogenerated CLI plugins for package for particular command. + + Args: + package: Package. + command: Name of command to remove CLI. + Returns: + None + """ + + if package.metadata.yang_module_str is None: + return + if f'auto-generate-{command}' not in package.manifest['cli']: + return + if not package.manifest['cli'][f'auto-generate-{command}']: + return + module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str) + self.cli_gen.remove_cli_plugin(command, module_name) + log.debug(f'{command} command line interface removed for {module_name}') def _post_operation_hook(self): """ Common operations executed after service is created/removed. """ diff --git a/sonic_package_manager/service_creator/feature.py b/sonic_package_manager/service_creator/feature.py index 97960a327d66..eb8e1a0710e0 100644 --- a/sonic_package_manager/service_creator/feature.py +++ b/sonic_package_manager/service_creator/feature.py @@ -144,15 +144,3 @@ def get_non_configurable_feature_entries(manifest) -> Dict[str, str]: 'has_global_scope': str(manifest['service']['host-service']), 'has_timer': str(manifest['service']['delayed']), } - - def _get_tables(self): - tables = [] - running = self._sonic_db.running_table(FEATURE) - if running is not None: # it's Ok if there is no database container running - tables.append(running) - persistent = self._sonic_db.persistent_table(FEATURE) - if persistent is not None: # it's Ok if there is no config_db.json - tables.append(persistent) - tables.append(self._sonic_db.initial_table(FEATURE)) # init_cfg.json is must - - return tables diff --git a/tests/sonic_package_manager/conftest.py b/tests/sonic_package_manager/conftest.py index 2788a75cd3f2..1ec067657c99 100644 --- a/tests/sonic_package_manager/conftest.py +++ b/tests/sonic_package_manager/conftest.py @@ -7,6 +7,8 @@ import pytest from docker_image.reference import Reference +from config.config_mgmt import ConfigMgmt + from sonic_package_manager.database import PackageDatabase, PackageEntry from sonic_package_manager.manager import DockerApi, PackageManager from sonic_package_manager.manifest import Manifest @@ -62,7 +64,17 @@ def mock_service_creator(): @pytest.fixture def mock_sonic_db(): - yield Mock() + yield MagicMock() + + +@pytest.fixture +def mock_config_mgmt(): + yield MagicMock() + + +@pytest.fixture +def mock_cli_gen(): + yield MagicMock() @pytest.fixture @@ -107,7 +119,7 @@ def __init__(self): 'before': ['swss'], } ) - self.add('Azure/docker-test', '1.6.0', 'test-package', '1.6.0') + self.add('Azure/docker-test', '1.6.0', 'test-package', '1.6.0', yang='TEST') self.add('Azure/docker-test-2', '1.5.0', 'test-package-2', '1.5.0') self.add('Azure/docker-test-2', '2.0.0', 'test-package-2', '2.0.0') self.add('Azure/docker-test-3', 'latest', 'test-package-3', '1.6.0') @@ -124,23 +136,26 @@ def __init__(self): def from_registry(self, repository: str, reference: str): manifest = Manifest.marshal(self.metadata_store[repository][reference]['manifest']) components = self.metadata_store[repository][reference]['components'] - return Metadata(manifest, components) + yang = self.metadata_store[repository][reference]['yang'] + return Metadata(manifest, components, yang) def from_local(self, image: str): ref = Reference.parse(image) manifest = Manifest.marshal(self.metadata_store[ref['name']][ref['tag']]['manifest']) components = self.metadata_store[ref['name']][ref['tag']]['components'] - return Metadata(manifest, components) + yang = self.metadata_store[ref['name']][ref['tag']]['yang'] + return Metadata(manifest, components, yang) def from_tarball(self, filepath: str) -> Manifest: path, ref = filepath.split(':') manifest = Manifest.marshal(self.metadata_store[path][ref]['manifest']) components = self.metadata_store[path][ref]['components'] - return Metadata(manifest, components) + yang = self.metadata_store[path][ref]['yang'] + return Metadata(manifest, components, yang) def add(self, repo, reference, name, version, components=None, warm_shutdown=None, fast_shutdown=None, - processes=None): + processes=None, yang=None): repo_dict = self.metadata_store.setdefault(repo, {}) repo_dict[reference] = { 'manifest': { @@ -157,6 +172,7 @@ def add(self, repo, reference, name, version, components=None, 'processes': processes or [], }, 'components': components or {}, + 'yang': yang, } yield FakeMetadataResolver() @@ -252,7 +268,7 @@ def fake_db(fake_metadata_resolver): description='SONiC Package Manager Test Package', default_reference='1.6.0', installed=False, - built_in=False + built_in=False, ) add_package( content, @@ -402,8 +418,8 @@ def sonic_fs(fs): @pytest.fixture(autouse=True) def patch_pkgutil(): - with mock.patch('pkgutil.get_loader'): - yield + with mock.patch('pkgutil.get_loader') as loader: + yield loader @pytest.fixture diff --git a/tests/sonic_package_manager/test_service_creator.py b/tests/sonic_package_manager/test_service_creator.py index 3e1a32b4d1af..9f496783a936 100644 --- a/tests/sonic_package_manager/test_service_creator.py +++ b/tests/sonic_package_manager/test_service_creator.py @@ -60,13 +60,25 @@ def manifest(): }) -def test_service_creator(sonic_fs, manifest, package_manager, mock_feature_registry, mock_sonic_db): - creator = ServiceCreator(mock_feature_registry, mock_sonic_db) +@pytest.fixture() +def service_creator(mock_feature_registry, + mock_sonic_db, + mock_cli_gen, + mock_config_mgmt): + yield ServiceCreator( + mock_feature_registry, + mock_sonic_db, + mock_cli_gen, + mock_config_mgmt + ) + + +def test_service_creator(sonic_fs, manifest, service_creator, package_manager): entry = PackageEntry('test', 'azure/sonic-test') package = Package(entry, Metadata(manifest)) installed_packages = package_manager._get_installed_packages_and(package) - creator.create(package) - creator.generate_shutdown_sequence_files(installed_packages) + service_creator.create(package) + service_creator.generate_shutdown_sequence_files(installed_packages) assert sonic_fs.exists(os.path.join(ETC_SONIC_PATH, 'swss_dependent')) assert sonic_fs.exists(os.path.join(DOCKER_CTL_SCRIPT_LOCATION, 'test.sh')) @@ -82,73 +94,116 @@ def read_file(name): assert read_file('test_reconcile') == 'test-process test-process-3' -def test_service_creator_with_timer_unit(sonic_fs, manifest, mock_feature_registry, mock_sonic_db): - creator = ServiceCreator(mock_feature_registry, mock_sonic_db) +def test_service_creator_with_timer_unit(sonic_fs, manifest, service_creator): entry = PackageEntry('test', 'azure/sonic-test') package = Package(entry, Metadata(manifest)) - creator.create(package) + service_creator.create(package) assert not sonic_fs.exists(os.path.join(SYSTEMD_LOCATION, 'test.timer')) manifest['service']['delayed'] = True package = Package(entry, Metadata(manifest)) - creator.create(package) + service_creator.create(package) assert sonic_fs.exists(os.path.join(SYSTEMD_LOCATION, 'test.timer')) -def test_service_creator_with_debug_dump(sonic_fs, manifest, mock_feature_registry, mock_sonic_db): - creator = ServiceCreator(mock_feature_registry, mock_sonic_db) +def test_service_creator_with_debug_dump(sonic_fs, manifest, service_creator): entry = PackageEntry('test', 'azure/sonic-test') package = Package(entry, Metadata(manifest)) - creator.create(package) + service_creator.create(package) assert not sonic_fs.exists(os.path.join(DEBUG_DUMP_SCRIPT_LOCATION, 'test')) manifest['package']['debug-dump'] = '/some/command' package = Package(entry, Metadata(manifest)) - creator.create(package) + service_creator.create(package) assert sonic_fs.exists(os.path.join(DEBUG_DUMP_SCRIPT_LOCATION, 'test')) -def test_service_creator_initial_config(sonic_fs, manifest, mock_feature_registry, mock_sonic_db): +def test_service_creator_yang(sonic_fs, manifest, mock_sonic_db, + mock_config_mgmt, service_creator): + test_yang = 'TEST YANG' + test_yang_module = 'sonic-test' + mock_connector = Mock() - mock_connector.get_config = Mock(return_value={}) mock_sonic_db.get_connectors = Mock(return_value=[mock_connector]) - - creator = ServiceCreator(mock_feature_registry, mock_sonic_db) + mock_connector.get_table = Mock(return_value={'key_a': {'field_1': 'value_1'}}) + mock_connector.get_config = Mock(return_value={ + 'TABLE_A': mock_connector.get_table('') + }) entry = PackageEntry('test', 'azure/sonic-test') - package = Package(entry, Metadata(manifest)) - creator.create(package) + package = Package(entry, Metadata(manifest, yang_module_str=test_yang)) + service_creator.create(package) - assert not sonic_fs.exists(os.path.join(DEBUG_DUMP_SCRIPT_LOCATION, 'test')) + mock_config_mgmt.add_module.assert_called_with(test_yang) + mock_config_mgmt.get_module_name = Mock(return_value=test_yang_module) manifest['package']['init-cfg'] = { 'TABLE_A': { 'key_a': { - 'field_1': 'value_1', + 'field_1': 'new_value_1', 'field_2': 'value_2' }, }, } - package = Package(entry, Metadata(manifest)) + package = Package(entry, Metadata(manifest, yang_module_str=test_yang)) + + service_creator.create(package) + + mock_config_mgmt.add_module.assert_called_with(test_yang) - creator.create(package) mock_connector.mod_config.assert_called_with( { 'TABLE_A': { - 'key_a': { - 'field_1': 'value_1', - 'field_2': 'value_2', - }, - }, - } + 'key_a': { + 'field_1': 'value_1', + 'field_2': 'value_2', + }, + }, + } ) - creator.remove(package) + mock_config_mgmt.sy.confDbYangMap = { + 'TABLE_A': {'module': test_yang_module} + } + + service_creator.remove(package) mock_connector.set_entry.assert_called_with('TABLE_A', 'key_a', None) + mock_config_mgmt.remove_module.assert_called_with(test_yang_module) + + +def test_service_creator_autocli(sonic_fs, manifest, mock_cli_gen, + mock_config_mgmt, service_creator): + test_yang = 'TEST YANG' + test_yang_module = 'sonic-test' + + manifest['cli']['auto-generate-show'] = True + manifest['cli']['auto-generate-config'] = True + + entry = PackageEntry('test', 'azure/sonic-test') + package = Package(entry, Metadata(manifest, yang_module_str=test_yang)) + mock_config_mgmt.get_module_name = Mock(return_value=test_yang_module) + service_creator.create(package) + + mock_cli_gen.generate_cli_plugin.assert_has_calls( + [ + call('show', test_yang_module), + call('config', test_yang_module), + ], + any_order=True + ) + + service_creator.remove(package) + mock_cli_gen.remove_cli_plugin.assert_has_calls( + [ + call('show', test_yang_module), + call('config', test_yang_module), + ], + any_order=True + ) def test_feature_registration(mock_sonic_db, manifest):