Skip to content

Commit 02ce8d6

Browse files
[sonic-package-manager] update FEATURE entries on upgrade (#1803)
Signed-off-by: Stepan Blyschak [email protected] What I did Implemented feature table update on package upgrade. It could be that upgraded package includes timer service when old one does not. How I did it Upgrade logic added in manager.py Use ConfigDBConnector for config modification as it simplifies config DB operations. How to verify it Upgrade package with new feature attributes.
1 parent 9f123c0 commit 02ce8d6

File tree

7 files changed

+313
-190
lines changed

7 files changed

+313
-190
lines changed

sonic_package_manager/manager.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,6 @@ def upgrade_from_source(self,
534534
)
535535

536536
old_feature = old_package.manifest['service']['name']
537-
new_feature = new_package.manifest['service']['name']
538537
old_version = old_package.manifest['package']['version']
539538
new_version = new_package.manifest['package']['version']
540539

@@ -577,7 +576,12 @@ def upgrade_from_source(self,
577576
source.install(new_package)
578577
exits.callback(rollback(source.uninstall, new_package))
579578

580-
if self.feature_registry.is_feature_enabled(old_feature):
579+
feature_enabled = self.feature_registry.is_feature_enabled(old_feature)
580+
581+
if feature_enabled:
582+
self._systemctl_action(old_package, 'disable')
583+
exits.callback(rollback(self._systemctl_action,
584+
old_package, 'enable'))
581585
self._systemctl_action(old_package, 'stop')
582586
exits.callback(rollback(self._systemctl_action,
583587
old_package, 'start'))
@@ -600,14 +604,27 @@ def upgrade_from_source(self,
600604
self._get_installed_packages_and(old_package))
601605
)
602606

603-
if self.feature_registry.is_feature_enabled(new_feature):
607+
# If old feature was enabled, the user should have the new feature enabled as well.
608+
if feature_enabled:
609+
self._systemctl_action(new_package, 'enable')
610+
exits.callback(rollback(self._systemctl_action,
611+
new_package, 'disable'))
604612
self._systemctl_action(new_package, 'start')
605613
exits.callback(rollback(self._systemctl_action,
606614
new_package, 'stop'))
607615

616+
# Update feature configuration after we have started new service.
617+
# If we place it before the above, our service start/stop will
618+
# interfere with hostcfgd in rollback path leading to a service
619+
# running with new image and not the old one.
620+
self.feature_registry.update(old_package.manifest, new_package.manifest)
621+
exits.callback(rollback(
622+
self.feature_registry.update, new_package.manifest, old_package.manifest)
623+
)
624+
608625
if not skip_host_plugins:
609626
self._install_cli_plugins(new_package)
610-
exits.callback(rollback(self._uninstall_cli_plugin, old_package))
627+
exits.callback(rollback(self._uninstall_cli_plugin, new_package))
611628

612629
self.docker.rmi(old_package.image_id, force=True)
613630

sonic_package_manager/metadata.py

+2-21
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,12 @@
66
import tarfile
77
from typing import Dict
88

9+
from sonic_package_manager import utils
910
from sonic_package_manager.errors import MetadataError
1011
from sonic_package_manager.manifest import Manifest
1112
from sonic_package_manager.version import Version
1213

1314

14-
def deep_update(dst: Dict, src: Dict) -> Dict:
15-
""" Deep update dst dictionary with src dictionary.
16-
17-
Args:
18-
dst: Dictionary to update
19-
src: Dictionary to update with
20-
21-
Returns:
22-
New merged dictionary.
23-
"""
24-
25-
for key, value in src.items():
26-
if isinstance(value, dict):
27-
node = dst.setdefault(key, {})
28-
deep_update(node, value)
29-
else:
30-
dst[key] = value
31-
return dst
32-
33-
3415
def translate_plain_to_tree(plain: Dict[str, str], sep='.') -> Dict:
3516
""" Convert plain key/value dictionary into
3617
a tree by spliting the key with '.'
@@ -62,7 +43,7 @@ def translate_plain_to_tree(plain: Dict[str, str], sep='.') -> Dict:
6243
continue
6344
namespace, key = key.split(sep, 1)
6445
res.setdefault(namespace, {})
65-
deep_update(res[namespace], translate_plain_to_tree({key: value}))
46+
utils.deep_update(res[namespace], translate_plain_to_tree({key: value}))
6647
return res
6748

6849

sonic_package_manager/service_creator/creator.py

+14-42
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from prettyprinter import pformat
1212
from toposort import toposort_flatten, CircularDependencyError
1313

14+
from sonic_package_manager import utils
1415
from sonic_package_manager.logger import log
1516
from sonic_package_manager.package import Package
1617
from sonic_package_manager.service_creator import ETC_SONIC_PATH
@@ -468,21 +469,14 @@ def set_initial_config(self, package):
468469
"""
469470

470471
init_cfg = package.manifest['package']['init-cfg']
472+
if not init_cfg:
473+
return
471474

472-
for tablename, content in init_cfg.items():
473-
if not isinstance(content, dict):
474-
continue
475-
476-
tables = self._get_tables(tablename)
477-
478-
for key in content:
479-
for table in tables:
480-
cfg = content[key]
481-
exists, old_fvs = table.get(key)
482-
if exists:
483-
cfg.update(old_fvs)
484-
fvs = list(cfg.items())
485-
table.set(key, fvs)
475+
for conn in self.sonic_db.get_connectors():
476+
cfg = conn.get_config()
477+
new_cfg = init_cfg.copy()
478+
utils.deep_update(new_cfg, cfg)
479+
conn.mod_config(new_cfg)
486480

487481
def remove_config(self, package):
488482
""" Remove configuration based on init-cfg tables, so having
@@ -497,35 +491,13 @@ def remove_config(self, package):
497491
"""
498492

499493
init_cfg = package.manifest['package']['init-cfg']
494+
if not init_cfg:
495+
return
500496

501-
for tablename, content in init_cfg.items():
502-
if not isinstance(content, dict):
503-
continue
504-
505-
tables = self._get_tables(tablename)
506-
507-
for key in content:
508-
for table in tables:
509-
table._del(key)
510-
511-
def _get_tables(self, table_name):
512-
""" Return swsscommon Tables for all kinds of configuration DBs """
513-
514-
tables = []
515-
516-
running_table = self.sonic_db.running_table(table_name)
517-
if running_table is not None:
518-
tables.append(running_table)
519-
520-
persistent_table = self.sonic_db.persistent_table(table_name)
521-
if persistent_table is not None:
522-
tables.append(persistent_table)
523-
524-
initial_table = self.sonic_db.initial_table(table_name)
525-
if initial_table is not None:
526-
tables.append(initial_table)
527-
528-
return tables
497+
for conn in self.sonic_db.get_connectors():
498+
for table in init_cfg:
499+
for key in init_cfg[table]:
500+
conn.set_entry(table, key, None)
529501

530502
def _post_operation_hook(self):
531503
""" Common operations executed after service is created/removed. """

sonic_package_manager/service_creator/feature.py

+75-25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
}
1717

1818

19+
def is_enabled(cfg):
20+
return cfg.get('state', 'disabled').lower() == 'enabled'
21+
22+
23+
def is_multi_instance(cfg):
24+
return str(cfg.get('has_per_asic_scope', 'False')).lower() == 'true'
25+
26+
1927
class FeatureRegistry:
2028
""" FeatureRegistry class provides an interface to
2129
register/de-register new feature persistently. """
@@ -27,51 +35,93 @@ def register(self,
2735
manifest: Manifest,
2836
state: str = 'disabled',
2937
owner: str = 'local'):
38+
""" Register feature in CONFIG DBs.
39+
40+
Args:
41+
manifest: Feature's manifest.
42+
state: Desired feature admin state.
43+
owner: Owner of this feature (kube/local).
44+
Returns:
45+
None.
46+
"""
47+
3048
name = manifest['service']['name']
31-
for table in self._get_tables():
32-
cfg_entries = self.get_default_feature_entries(state, owner)
33-
non_cfg_entries = self.get_non_configurable_feature_entries(manifest)
49+
db_connectors = self._sonic_db.get_connectors()
50+
cfg_entries = self.get_default_feature_entries(state, owner)
51+
non_cfg_entries = self.get_non_configurable_feature_entries(manifest)
3452

35-
exists, current_cfg = table.get(name)
53+
for conn in db_connectors:
54+
current_cfg = conn.get_entry(FEATURE, name)
3655

3756
new_cfg = cfg_entries.copy()
3857
# Override configurable entries with CONFIG DB data.
39-
new_cfg = {**new_cfg, **dict(current_cfg)}
58+
new_cfg = {**new_cfg, **current_cfg}
4059
# Override CONFIG DB data with non configurable entries.
4160
new_cfg = {**new_cfg, **non_cfg_entries}
4261

43-
table.set(name, list(new_cfg.items()))
62+
conn.set_entry(FEATURE, name, new_cfg)
4463

4564
def deregister(self, name: str):
46-
for table in self._get_tables():
47-
table._del(name)
65+
""" Deregister feature by name.
66+
67+
Args:
68+
name: Name of the feature in CONFIG DB.
69+
Returns:
70+
None
71+
"""
72+
73+
db_connetors = self._sonic_db.get_connectors()
74+
for conn in db_connetors:
75+
conn.set_entry(FEATURE, name, None)
76+
77+
def update(self,
78+
old_manifest: Manifest,
79+
new_manifest: Manifest):
80+
""" Migrate feature configuration. It can be that non-configurable
81+
feature entries have to be updated. e.g: "has_timer" for example if
82+
the new feature introduces a service timer or name of the service has
83+
changed, but user configurable entries are not changed).
84+
85+
Args:
86+
old_manifest: Old feature manifest.
87+
new_manifest: New feature manifest.
88+
Returns:
89+
None
90+
"""
91+
92+
old_name = old_manifest['service']['name']
93+
new_name = new_manifest['service']['name']
94+
db_connectors = self._sonic_db.get_connectors()
95+
non_cfg_entries = self.get_non_configurable_feature_entries(new_manifest)
96+
97+
for conn in db_connectors:
98+
current_cfg = conn.get_entry(FEATURE, old_name)
99+
conn.set_entry(FEATURE, old_name, None)
100+
101+
new_cfg = current_cfg.copy()
102+
# Override CONFIG DB data with non configurable entries.
103+
new_cfg = {**new_cfg, **non_cfg_entries}
104+
105+
conn.set_entry(FEATURE, new_name, new_cfg)
48106

49107
def is_feature_enabled(self, name: str) -> bool:
50108
""" Returns whether the feature is current enabled
51109
or not. Accesses running CONFIG DB. If no running CONFIG_DB
52110
table is found in tables returns False. """
53111

54-
running_db_table = self._sonic_db.running_table(FEATURE)
55-
if running_db_table is None:
112+
conn = self._sonic_db.get_running_db_connector()
113+
if conn is None:
56114
return False
57115

58-
exists, cfg = running_db_table.get(name)
59-
if not exists:
60-
return False
61-
cfg = dict(cfg)
62-
return cfg.get('state').lower() == 'enabled'
116+
cfg = conn.get_entry(FEATURE, name)
117+
return is_enabled(cfg)
63118

64119
def get_multi_instance_features(self):
65-
res = []
66-
init_db_table = self._sonic_db.initial_table(FEATURE)
67-
for feature in init_db_table.keys():
68-
exists, cfg = init_db_table.get(feature)
69-
assert exists
70-
cfg = dict(cfg)
71-
asic_flag = str(cfg.get('has_per_asic_scope', 'False'))
72-
if asic_flag.lower() == 'true':
73-
res.append(feature)
74-
return res
120+
""" Returns a list of features which run in asic namespace. """
121+
122+
conn = self._sonic_db.get_initial_db_connector()
123+
features = conn.get_table(FEATURE)
124+
return [feature for feature, cfg in features.items() if is_multi_instance(cfg)]
75125

76126
@staticmethod
77127
def get_default_feature_entries(state=None, owner=None) -> Dict[str, str]:

0 commit comments

Comments
 (0)