Skip to content

Commit f5e5a56

Browse files
[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 <[email protected]> Co-authored-by: Vadym Hlushko <[email protected]>
1 parent 64777a4 commit f5e5a56

File tree

9 files changed

+380
-96
lines changed

9 files changed

+380
-96
lines changed

config/config_mgmt.py

+91-19
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
config_mgmt.py provides classes for configuration validation and for Dynamic
33
Port Breakout.
44
'''
5+
6+
import os
57
import re
8+
import shutil
69
import syslog
10+
import tempfile
11+
import yang as ly
712
from json import load
813
from sys import flags
914
from time import sleep as tsleep
@@ -46,34 +51,38 @@ def __init__(self, source="configDB", debug=False, allowTablesWithoutYang=True):
4651
try:
4752
self.configdbJsonIn = None
4853
self.configdbJsonOut = None
54+
self.source = source
4955
self.allowTablesWithoutYang = allowTablesWithoutYang
5056

5157
# logging vars
5258
self.SYSLOG_IDENTIFIER = "ConfigMgmt"
5359
self.DEBUG = debug
5460

55-
self.sy = sonic_yang.SonicYang(YANG_DIR, debug=debug)
56-
# load yang models
57-
self.sy.loadYangModel()
58-
# load jIn from config DB or from config DB json file.
59-
if source.lower() == 'configdb':
60-
self.readConfigDB()
61-
# treat any other source as file input
62-
else:
63-
self.readConfigDBJson(source)
64-
# this will crop config, xlate and load.
65-
self.sy.loadData(self.configdbJsonIn)
66-
67-
# Raise if tables without YANG models are not allowed but exist.
68-
if not allowTablesWithoutYang and len(self.sy.tablesWithOutYang):
69-
raise Exception('Config has tables without YANG models')
61+
self.__init_sonic_yang()
7062

7163
except Exception as e:
7264
self.sysLog(doPrint=True, logLevel=syslog.LOG_ERR, msg=str(e))
7365
raise Exception('ConfigMgmt Class creation failed')
7466

7567
return
7668

69+
def __init_sonic_yang(self):
70+
self.sy = sonic_yang.SonicYang(YANG_DIR, debug=self.DEBUG)
71+
# load yang models
72+
self.sy.loadYangModel()
73+
# load jIn from config DB or from config DB json file.
74+
if self.source.lower() == 'configdb':
75+
self.readConfigDB()
76+
# treat any other source as file input
77+
else:
78+
self.readConfigDBJson(self.source)
79+
# this will crop config, xlate and load.
80+
self.sy.loadData(self.configdbJsonIn)
81+
82+
# Raise if tables without YANG models are not allowed but exist.
83+
if not self.allowTablesWithoutYang and len(self.sy.tablesWithOutYang):
84+
raise Exception('Config has tables without YANG models')
85+
7786
def __del__(self):
7887
pass
7988

@@ -213,6 +222,69 @@ def writeConfigDB(self, jDiff):
213222

214223
return
215224

225+
def add_module(self, yang_module_str):
226+
"""
227+
Validate and add new YANG module to the system.
228+
229+
Parameters:
230+
yang_module_str (str): YANG module in string representation.
231+
232+
Returns:
233+
None
234+
"""
235+
236+
module_name = self.get_module_name(yang_module_str)
237+
module_path = os.path.join(YANG_DIR, '{}.yang'.format(module_name))
238+
if os.path.exists(module_path):
239+
raise Exception('{} already exists'.format(module_name))
240+
with open(module_path, 'w') as module_file:
241+
module_file.write(yang_module_str)
242+
try:
243+
self.__init_sonic_yang()
244+
except Exception:
245+
os.remove(module_path)
246+
raise
247+
248+
def remove_module(self, module_name):
249+
"""
250+
Remove YANG module from the system and validate.
251+
252+
Parameters:
253+
module_name (str): YANG module name.
254+
255+
Returns:
256+
None
257+
"""
258+
259+
module_path = os.path.join(YANG_DIR, '{}.yang'.format(module_name))
260+
if not os.path.exists(module_path):
261+
return
262+
temp = tempfile.NamedTemporaryFile(delete=False)
263+
try:
264+
shutil.move(module_path, temp.name)
265+
self.__init_sonic_yang()
266+
except Exception:
267+
shutil.move(temp.name, module_path)
268+
raise
269+
270+
@staticmethod
271+
def get_module_name(yang_module_str):
272+
"""
273+
Read yangs module name from yang_module_str
274+
275+
Parameters:
276+
yang_module_str(str): YANG module string.
277+
278+
Returns:
279+
str: Module name
280+
"""
281+
282+
# Instantiate new context since parse_module_mem() loads the module into context.
283+
sy = sonic_yang.SonicYang(YANG_DIR)
284+
module = sy.ctx.parse_module_mem(yang_module_str, ly.LYS_IN_YANG)
285+
return module.name()
286+
287+
216288
# End of Class ConfigMgmt
217289

218290
class ConfigMgmtDPB(ConfigMgmt):
@@ -417,8 +489,8 @@ def _deletePorts(self, ports=list(), force=False):
417489
deps.extend(dep)
418490

419491
# No further action with no force and deps exist
420-
if force == False and deps:
421-
return configToLoad, deps, False;
492+
if not force and deps:
493+
return configToLoad, deps, False
422494

423495
# delets all deps, No topological sort is needed as of now, if deletion
424496
# of deps fails, return immediately
@@ -436,8 +508,8 @@ def _deletePorts(self, ports=list(), force=False):
436508
self.sy.deleteNode(str(xPathPort))
437509

438510
# Let`s Validate the tree now
439-
if self.validateConfigData()==False:
440-
return configToLoad, deps, False;
511+
if not self.validateConfigData():
512+
return configToLoad, deps, False
441513

442514
# All great if we are here, Lets get the diff
443515
self.configdbJsonOut = self.sy.getData()

sonic_package_manager/main.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,11 @@ def reset(ctx, name, force, yes, skip_host_plugins):
414414

415415
@cli.command()
416416
@add_options(PACKAGE_COMMON_OPERATION_OPTIONS)
417+
@click.option('--keep-config', is_flag=True, help='Keep features configuration in CONFIG DB.')
417418
@click.argument('name')
418419
@click.pass_context
419420
@root_privileges_required
420-
def uninstall(ctx, name, force, yes):
421+
def uninstall(ctx, name, force, yes, keep_config):
421422
""" Uninstall package. """
422423

423424
manager: PackageManager = ctx.obj
@@ -428,6 +429,7 @@ def uninstall(ctx, name, force, yes):
428429

429430
uninstall_opts = {
430431
'force': force,
432+
'keep_config': keep_config,
431433
}
432434

433435
try:

sonic_package_manager/manager.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010

1111
import docker
1212
import filelock
13+
from config import config_mgmt
1314
from sonic_py_common import device_info
1415

16+
from sonic_cli_gen.generator import CliGenerator
17+
1518
from sonic_package_manager import utils
1619
from sonic_package_manager.constraint import (
1720
VersionConstraint,
@@ -45,7 +48,10 @@
4548
run_command
4649
)
4750
from sonic_package_manager.service_creator.feature import FeatureRegistry
48-
from sonic_package_manager.service_creator.sonic_db import SonicDB
51+
from sonic_package_manager.service_creator.sonic_db import (
52+
INIT_CFG_JSON,
53+
SonicDB
54+
)
4955
from sonic_package_manager.service_creator.utils import in_chroot
5056
from sonic_package_manager.source import (
5157
PackageSource,
@@ -435,13 +441,16 @@ def install_from_source(self,
435441

436442
@under_lock
437443
@opt_check
438-
def uninstall(self, name: str, force=False):
444+
def uninstall(self, name: str,
445+
force: bool = False,
446+
keep_config: bool = False):
439447
""" Uninstall SONiC Package referenced by name. The uninstallation
440448
can be forced if force argument is True.
441449
442450
Args:
443451
name: SONiC Package name.
444452
force: Force the installation.
453+
keep_config: Keep feature configuration in databases.
445454
Raises:
446455
PackageManagerError
447456
"""
@@ -482,7 +491,7 @@ def uninstall(self, name: str, force=False):
482491
self._systemctl_action(package, 'stop')
483492
self._systemctl_action(package, 'disable')
484493
self._uninstall_cli_plugins(package)
485-
self.service_creator.remove(package)
494+
self.service_creator.remove(package, keep_config=keep_config)
486495
self.service_creator.generate_shutdown_sequence_files(
487496
self._get_installed_packages_except(package)
488497
)
@@ -1000,9 +1009,13 @@ def get_manager() -> 'PackageManager':
10001009
docker_api = DockerApi(docker.from_env(), ProgressManager())
10011010
registry_resolver = RegistryResolver()
10021011
metadata_resolver = MetadataResolver(docker_api, registry_resolver)
1012+
cfg_mgmt = config_mgmt.ConfigMgmt(source=INIT_CFG_JSON)
1013+
cli_generator = CliGenerator(log)
10031014
feature_registry = FeatureRegistry(SonicDB)
10041015
service_creator = ServiceCreator(feature_registry,
1005-
SonicDB)
1016+
SonicDB,
1017+
cli_generator,
1018+
cfg_mgmt)
10061019

10071020
return PackageManager(docker_api,
10081021
registry_resolver,

sonic_package_manager/manifest.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ def unmarshal(self, value):
205205
ManifestField('mandatory', DefaultMarshaller(bool), False),
206206
ManifestField('show', DefaultMarshaller(str), ''),
207207
ManifestField('config', DefaultMarshaller(str), ''),
208-
ManifestField('clear', DefaultMarshaller(str), '')
208+
ManifestField('clear', DefaultMarshaller(str), ''),
209+
ManifestField('auto-generate-show', DefaultMarshaller(bool), False),
210+
ManifestField('auto-generate-config', DefaultMarshaller(bool), False),
209211
])
210212
])
211213

sonic_package_manager/metadata.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import json
66
import tarfile
7-
from typing import Dict
7+
from typing import Dict, Optional
88

99
from sonic_package_manager import utils
1010
from sonic_package_manager.errors import MetadataError
@@ -54,6 +54,7 @@ class Metadata:
5454

5555
manifest: Manifest
5656
components: Dict[str, Version] = field(default_factory=dict)
57+
yang_module_str: Optional[str] = None
5758

5859

5960
class MetadataResolver:
@@ -163,5 +164,6 @@ def from_labels(cls, labels: Dict[str, str]) -> Metadata:
163164
except ValueError as err:
164165
raise MetadataError(f'Failed to parse component version: {err}')
165166

166-
return Metadata(Manifest.marshal(manifest_dict), components)
167+
yang_module_str = sonic_metadata.get('yang-module')
167168

169+
return Metadata(Manifest.marshal(manifest_dict), components, yang_module_str)

0 commit comments

Comments
 (0)