Skip to content

Commit 7443b9e

Browse files
[sonic-package-manager] support extension with multiple YANG modules (#2752)
What I did I added support for application extensions to have multiple YANG modules recorded in the labels. How I did it Extended support for yang modules. Preserved backward compatibility with existing extensions. How to verify it UT.
1 parent 522c3a9 commit 7443b9e

File tree

5 files changed

+201
-29
lines changed

5 files changed

+201
-29
lines changed

sonic_package_manager/manifest.py

+2
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ def unmarshal(self, value):
232232
ManifestField('clear', ListMarshaller(str), []),
233233
ManifestField('auto-generate-show', DefaultMarshaller(bool), False),
234234
ManifestField('auto-generate-config', DefaultMarshaller(bool), False),
235+
ManifestArray('auto-generate-show-source-yang-modules', DefaultMarshaller(str)),
236+
ManifestArray('auto-generate-config-source-yang-modules', DefaultMarshaller(str)),
235237
])
236238
])
237239

sonic_package_manager/metadata.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

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

99
from sonic_package_manager import utils
1010
from sonic_package_manager.errors import MetadataError
11+
from sonic_package_manager.logger import log
1112
from sonic_package_manager.manifest import Manifest
1213
from sonic_package_manager.version import Version
1314

@@ -54,7 +55,7 @@ class Metadata:
5455

5556
manifest: Manifest
5657
components: Dict[str, Version] = field(default_factory=dict)
57-
yang_module_str: Optional[str] = None
58+
yang_modules: List[str] = field(default_factory=list)
5859

5960

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

167-
yang_module_str = sonic_metadata.get('yang-module')
168+
labels_yang_modules = sonic_metadata.get('yang-module')
169+
yang_modules = []
168170

169-
return Metadata(Manifest.marshal(manifest_dict), components, yang_module_str)
171+
if isinstance(labels_yang_modules, str):
172+
yang_modules.append(labels_yang_modules)
173+
log.debug("Found one YANG module")
174+
elif isinstance(labels_yang_modules, dict):
175+
yang_modules.extend(labels_yang_modules.values())
176+
log.debug(f"Found YANG modules: {labels_yang_modules.keys()}")
177+
else:
178+
log.debug("No YANG modules found")
179+
180+
return Metadata(Manifest.marshal(manifest_dict), components, yang_modules)

sonic_package_manager/service_creator/creator.py

+41-22
Original file line numberDiff line numberDiff line change
@@ -518,18 +518,19 @@ def remove_config(self, package):
518518
None
519519
"""
520520

521-
if not package.metadata.yang_module_str:
521+
if not package.metadata.yang_modules:
522522
return
523523

524-
module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str)
525-
for tablename, module in self.cfg_mgmt.sy.confDbYangMap.items():
526-
if module.get('module') != module_name:
527-
continue
524+
for module in package.metadata.yang_modules:
525+
module_name = self.cfg_mgmt.get_module_name(module)
526+
for tablename, module in self.cfg_mgmt.sy.confDbYangMap.items():
527+
if module.get('module') != module_name:
528+
continue
528529

529-
for conn in self.sonic_db.get_connectors():
530-
keys = conn.get_table(tablename).keys()
531-
for key in keys:
532-
conn.set_entry(tablename, key, None)
530+
for conn in self.sonic_db.get_connectors():
531+
keys = conn.get_table(tablename).keys()
532+
for key in keys:
533+
conn.set_entry(tablename, key, None)
533534

534535
def validate_config(self, config):
535536
""" Validate configuration through YANG.
@@ -560,10 +561,11 @@ def install_yang_module(self, package: Package):
560561
None
561562
"""
562563

563-
if not package.metadata.yang_module_str:
564+
if not package.metadata.yang_modules:
564565
return
565566

566-
self.cfg_mgmt.add_module(package.metadata.yang_module_str)
567+
for module in package.metadata.yang_modules:
568+
self.cfg_mgmt.add_module(module)
567569

568570
def uninstall_yang_module(self, package: Package):
569571
""" Uninstall package's yang module in the system.
@@ -574,11 +576,12 @@ def uninstall_yang_module(self, package: Package):
574576
None
575577
"""
576578

577-
if not package.metadata.yang_module_str:
579+
if not package.metadata.yang_modules:
578580
return
579581

580-
module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str)
581-
self.cfg_mgmt.remove_module(module_name)
582+
for module in package.metadata.yang_modules:
583+
module_name = self.cfg_mgmt.get_module_name(module)
584+
self.cfg_mgmt.remove_module(module_name)
582585

583586
def install_autogen_cli_all(self, package: Package):
584587
""" Install autogenerated CLI plugins for package.
@@ -614,15 +617,16 @@ def install_autogen_cli(self, package: Package, command: str):
614617
None
615618
"""
616619

617-
if package.metadata.yang_module_str is None:
620+
if not package.metadata.yang_modules:
618621
return
619622
if f'auto-generate-{command}' not in package.manifest['cli']:
620623
return
621624
if not package.manifest['cli'][f'auto-generate-{command}']:
622625
return
623-
module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str)
624-
self.cli_gen.generate_cli_plugin(command, module_name)
625-
log.debug(f'{command} command line interface autogenerated for {module_name}')
626+
627+
for module_name in self._get_yang_modules_for_auto_gen(command, package):
628+
self.cli_gen.generate_cli_plugin(command, module_name)
629+
log.debug(f'{command} command line interface autogenerated for {module_name}')
626630

627631
def uninstall_autogen_cli(self, package: Package, command: str):
628632
""" Uninstall autogenerated CLI plugins for package for particular command.
@@ -634,18 +638,33 @@ def uninstall_autogen_cli(self, package: Package, command: str):
634638
None
635639
"""
636640

637-
if package.metadata.yang_module_str is None:
641+
if not package.metadata.yang_modules:
638642
return
639643
if f'auto-generate-{command}' not in package.manifest['cli']:
640644
return
641645
if not package.manifest['cli'][f'auto-generate-{command}']:
642646
return
643-
module_name = self.cfg_mgmt.get_module_name(package.metadata.yang_module_str)
644-
self.cli_gen.remove_cli_plugin(command, module_name)
645-
log.debug(f'{command} command line interface removed for {module_name}')
647+
648+
for module_name in self._get_yang_modules_for_auto_gen(command, package):
649+
self.cli_gen.remove_cli_plugin(command, module_name)
650+
log.debug(f'{command} command line interface removed for {module_name}')
646651

647652
def _post_operation_hook(self):
648653
""" Common operations executed after service is created/removed. """
649654

650655
if not in_chroot():
651656
run_command(['systemctl', 'daemon-reload'])
657+
658+
def _get_yang_modules_for_auto_gen(self, command: str, package: Package):
659+
source_yang_modules = package.manifest['cli'][f'auto-generate-{command}-source-yang-modules']
660+
661+
def filter_yang_modules_for_auto_gen(module_name):
662+
if not source_yang_modules:
663+
return True
664+
if module_name in source_yang_modules:
665+
return True
666+
return False
667+
668+
filtered_yang_modules = filter(filter_yang_modules_for_auto_gen,
669+
map(self.cfg_mgmt.get_module_name, package.metadata.yang_modules))
670+
return list(filtered_yang_modules)

tests/sonic_package_manager/test_metadata.py

+52
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
#!/usr/bin/env python
22

3+
import json
34
import contextlib
45
from unittest.mock import Mock, MagicMock
56

7+
import pytest
8+
69
from sonic_package_manager.database import PackageEntry
710
from sonic_package_manager.errors import MetadataError
11+
from sonic_package_manager.manifest import Manifest
812
from sonic_package_manager.metadata import MetadataResolver
913
from sonic_package_manager.version import Version
1014

1115

16+
@pytest.fixture
17+
def manifest_str():
18+
return json.dumps({
19+
'package': {
20+
'name': 'test',
21+
'version': '1.0.0',
22+
},
23+
'service': {
24+
'name': 'test',
25+
'asic-service': False,
26+
'host-service': True,
27+
},
28+
'container': {
29+
'privileged': True,
30+
},
31+
})
32+
33+
1234
def test_metadata_resolver_local(mock_registry_resolver, mock_docker_api):
1335
metadata_resolver = MetadataResolver(mock_docker_api, mock_registry_resolver)
1436
# it raises exception because mock manifest is not a valid manifest
@@ -35,3 +57,33 @@ def return_mock_registry(repository):
3557
mock_registry.manifest.assert_called_once_with('test-repository', '1.2.0')
3658
mock_registry.blobs.assert_called_once_with('test-repository', 'some-digest')
3759
mock_docker_api.labels.assert_not_called()
60+
61+
62+
def test_metadata_construction(manifest_str):
63+
metadata = MetadataResolver.from_labels({
64+
'com': {
65+
'azure': {
66+
'sonic': {
67+
'manifest': manifest_str,
68+
'yang-module': 'TEST'
69+
}
70+
}
71+
}
72+
})
73+
assert metadata.yang_modules == ['TEST']
74+
75+
metadata = MetadataResolver.from_labels({
76+
'com': {
77+
'azure': {
78+
'sonic': {
79+
'manifest': manifest_str,
80+
'yang-module': {
81+
'sonic-test': 'TEST',
82+
'sonic-test-2': 'TEST 2',
83+
},
84+
},
85+
},
86+
},
87+
})
88+
assert metadata.yang_modules == ['TEST', 'TEST 2']
89+

tests/sonic_package_manager/test_service_creator.py

+91-3
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def test_service_creator_yang(sonic_fs, manifest, mock_sonic_db,
157157
})
158158

159159
entry = PackageEntry('test', 'azure/sonic-test')
160-
package = Package(entry, Metadata(manifest, yang_module_str=test_yang))
160+
package = Package(entry, Metadata(manifest, yang_modules=[test_yang]))
161161
service_creator.create(package)
162162

163163
mock_config_mgmt.add_module.assert_called_with(test_yang)
@@ -171,7 +171,7 @@ def test_service_creator_yang(sonic_fs, manifest, mock_sonic_db,
171171
},
172172
},
173173
}
174-
package = Package(entry, Metadata(manifest, yang_module_str=test_yang))
174+
package = Package(entry, Metadata(manifest, yang_modules=[test_yang]))
175175

176176
service_creator.create(package)
177177

@@ -190,6 +190,42 @@ def test_service_creator_yang(sonic_fs, manifest, mock_sonic_db,
190190
mock_config_mgmt.remove_module.assert_called_with(test_yang_module)
191191

192192

193+
def test_service_creator_multi_yang(sonic_fs, manifest, mock_config_mgmt, service_creator):
194+
test_yang = 'TEST YANG'
195+
test_yang_2 = 'TEST YANG 2'
196+
197+
def get_module_name(module_src):
198+
if module_src == test_yang:
199+
return 'sonic-test'
200+
elif module_src == test_yang_2:
201+
return 'sonic-test-2'
202+
else:
203+
raise ValueError(f'Unknown module {module_src}')
204+
205+
entry = PackageEntry('test', 'azure/sonic-test')
206+
package = Package(entry, Metadata(manifest, yang_modules=[test_yang, test_yang_2]))
207+
service_creator.create(package)
208+
209+
mock_config_mgmt.add_module.assert_has_calls(
210+
[
211+
call(test_yang),
212+
call(test_yang_2)
213+
],
214+
any_order=True,
215+
)
216+
217+
mock_config_mgmt.get_module_name = Mock(side_effect=get_module_name)
218+
219+
service_creator.remove(package)
220+
mock_config_mgmt.remove_module.assert_has_calls(
221+
[
222+
call(get_module_name(test_yang)),
223+
call(get_module_name(test_yang_2))
224+
],
225+
any_order=True,
226+
)
227+
228+
193229
def test_service_creator_autocli(sonic_fs, manifest, mock_cli_gen,
194230
mock_config_mgmt, service_creator):
195231
test_yang = 'TEST YANG'
@@ -199,7 +235,7 @@ def test_service_creator_autocli(sonic_fs, manifest, mock_cli_gen,
199235
manifest['cli']['auto-generate-config'] = True
200236

201237
entry = PackageEntry('test', 'azure/sonic-test')
202-
package = Package(entry, Metadata(manifest, yang_module_str=test_yang))
238+
package = Package(entry, Metadata(manifest, yang_modules=[test_yang]))
203239
mock_config_mgmt.get_module_name = Mock(return_value=test_yang_module)
204240
service_creator.create(package)
205241

@@ -226,6 +262,58 @@ def test_service_creator_post_operation_hook(sonic_fs, manifest, mock_sonic_db,
226262
service_creator._post_operation_hook()
227263
run_command.assert_called_with(['systemctl', 'daemon-reload'])
228264

265+
def test_service_creator_multi_yang_filter_auto_cli_modules(sonic_fs, manifest, mock_cli_gen,
266+
mock_config_mgmt, service_creator):
267+
test_yang = 'TEST YANG'
268+
test_yang_2 = 'TEST YANG 2'
269+
test_yang_3 = 'TEST YANG 3'
270+
test_yang_4 = 'TEST YANG 4'
271+
272+
def get_module_name(module_src):
273+
if module_src == test_yang:
274+
return 'sonic-test'
275+
elif module_src == test_yang_2:
276+
return 'sonic-test-2'
277+
elif module_src == test_yang_3:
278+
return 'sonic-test-3'
279+
elif module_src == test_yang_4:
280+
return 'sonic-test-4'
281+
else:
282+
raise ValueError(f'Unknown module {module_src}')
283+
284+
manifest['cli']['auto-generate-show'] = True
285+
manifest['cli']['auto-generate-config'] = True
286+
manifest['cli']['auto-generate-show-source-yang-modules'] = ['sonic-test-2', 'sonic-test-4']
287+
manifest['cli']['auto-generate-config-source-yang-modules'] = ['sonic-test-2', 'sonic-test-4']
288+
289+
entry = PackageEntry('test', 'azure/sonic-test')
290+
package = Package(entry, Metadata(manifest, yang_modules=[test_yang, test_yang_2, test_yang_3, test_yang_4]))
291+
mock_config_mgmt.get_module_name = Mock(side_effect=get_module_name)
292+
service_creator.create(package)
293+
294+
assert mock_cli_gen.generate_cli_plugin.call_count == 4
295+
mock_cli_gen.generate_cli_plugin.assert_has_calls(
296+
[
297+
call('show', get_module_name(test_yang_2)),
298+
call('show', get_module_name(test_yang_4)),
299+
call('config', get_module_name(test_yang_2)),
300+
call('config', get_module_name(test_yang_4)),
301+
],
302+
any_order=True
303+
)
304+
305+
service_creator.remove(package)
306+
assert mock_cli_gen.remove_cli_plugin.call_count == 4
307+
mock_cli_gen.remove_cli_plugin.assert_has_calls(
308+
[
309+
call('show', get_module_name(test_yang_2)),
310+
call('show', get_module_name(test_yang_4)),
311+
call('config', get_module_name(test_yang_2)),
312+
call('config', get_module_name(test_yang_4)),
313+
],
314+
any_order=True
315+
)
316+
229317
def test_feature_registration(mock_sonic_db, manifest):
230318
mock_connector = Mock()
231319
mock_connector.get_entry = Mock(return_value={})

0 commit comments

Comments
 (0)