Skip to content

Commit 432ef6e

Browse files
committed
Adding local-users-passwords-reset feature service, YANG model and its tests
1 parent 4f5cb87 commit 432ef6e

File tree

13 files changed

+390
-74
lines changed

13 files changed

+390
-74
lines changed

files/build_templates/init_cfg.json.j2

+5
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@
152152
"logout": ""
153153
}
154154
},
155+
"LOCAL_USERS_PASSWORDS_RESET": {
156+
"global": {
157+
"state": "disabled"
158+
}
159+
},
155160
"SYSTEM_DEFAULTS" : {
156161
{%- if include_mux == "y" %}
157162
"mux_tunnel_egress_acl": {

files/build_templates/sonic_debian_extension.j2

+9
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,15 @@ sudo cp $IMAGE_CONFIGS/hostname/hostname-config.service $FILESYSTEM_ROOT_USR_LIB
620620
echo "hostname-config.service" | sudo tee -a $GENERATED_SERVICE_FILE
621621
sudo cp $IMAGE_CONFIGS/hostname/hostname-config.sh $FILESYSTEM_ROOT/usr/bin/
622622

623+
{% if enable_local_users_passwords_reset == "y" %}
624+
# Copy local-users-passwords-reset configuration scripts
625+
sudo cp $IMAGE_CONFIGS/local-users-passwords-reset/local-users-passwords-reset.service $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM
626+
echo "local-users-passwords-reset.service" | sudo tee -a $GENERATED_SERVICE_FILE
627+
sudo cp $IMAGE_CONFIGS/local-users-passwords-reset/local-users-passwords-reset.py $FILESYSTEM_ROOT/usr/bin/
628+
# Set execute permissions only
629+
sudo chmod 100 $FILESYSTEM_ROOT/usr/bin/
630+
{% endif %}
631+
623632
# Copy banner configuration scripts
624633
sudo cp $IMAGE_CONFIGS/bannerconfig/banner-config.service $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM
625634
echo "banner-config.service" | sudo tee -a $GENERATED_SERVICE_FILE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
import os
3+
import syslog
4+
from swsscommon.swsscommon import ConfigDBConnector, DBConnector
5+
from swsscommon import swsscommon
6+
7+
8+
STATE_DB = "STATE_DB"
9+
10+
11+
def get_platform_local_users_passwords_reset():
12+
try:
13+
from sonic_platform.local_users_passwords_reset import LocalUsersConfigurationReset
14+
local_users_password_reset_class = LocalUsersConfigurationReset()
15+
except ImportError:
16+
syslog.syslog(syslog.LOG_WARNING, "LocalUsersConfigurationReset: sonic_platform package not installed. Unable to find platform local users passwords reset implementation")
17+
raise Exception('Local users passwords reset implementation is not defined')
18+
19+
return local_users_password_reset_class
20+
21+
22+
class LocalUsersConfigurationResetService:
23+
def __init__(self):
24+
state_db_conn = DBConnector(STATE_DB, 0)
25+
# Wait if the Warm/Fast boot is in progress
26+
if swsscommon.RestartWaiter.isAdvancedBootInProgress(state_db_conn):
27+
swsscommon.RestartWaiter.waitAdvancedBootDone()
28+
29+
self.config_db = ConfigDBConnector()
30+
self.config_db.connect(wait_for_init=True, retry_on=True)
31+
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
32+
33+
def get_feature_state(self):
34+
'''
35+
Check if the feature is enabled by reading the redis table
36+
'''
37+
table = self.config_db.get_table(swsscommon.CFG_LOCAL_USERS_PASSWORDS_RESET)
38+
if table:
39+
state = table.get('global', {}).get('state')
40+
return True if state == 'enabled' else False
41+
42+
return False
43+
44+
def start(self):
45+
'''
46+
If the feature is enabled then reset the password's using the platform
47+
specific implementation
48+
'''
49+
local_users_password_reset = get_platform_local_users_passwords_reset()
50+
feature_enabled = self.get_feature_state()
51+
syslog.syslog(syslog.LOG_INFO, 'Feature is {}'.format('enabled' if feature_enabled else 'disabled'))
52+
should_trigger = local_users_password_reset.should_trigger()
53+
if should_trigger and feature_enabled:
54+
local_users_password_reset.start()
55+
56+
57+
def main():
58+
LocalUsersConfigurationResetService().start()
59+
60+
61+
if __name__ == "__main__":
62+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[Unit]
2+
Description=Update Local users' passwords config based on configdb
3+
Requires=config-setup.service
4+
After=config-setup.service
5+
Before=systemd-logind.service sshd.service getty.target [email protected]
6+
7+
8+
[Service]
9+
Type=oneshot
10+
RemainAfterExit=no
11+
ExecStart=/usr/bin/local-users-passwords-reset.py
12+
13+
[Install]
14+
WantedBy=sonic.target
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#
2+
# Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES.
3+
# Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
#############################################################################
18+
# Mellanox
19+
#
20+
# Module contains an implementation of SONiC Platform Base API and
21+
# provides the local users' passwords reset functionality implementation.
22+
#
23+
#############################################################################
24+
25+
try:
26+
import json
27+
import subprocess
28+
29+
from sonic_platform_base.local_users_passwords_reset_base import LocalUsersConfigurationResetBase
30+
from sonic_py_common.logger import Logger
31+
from . import utils
32+
except ImportError as e:
33+
raise ImportError (str(e) + "- required module not found")
34+
35+
36+
# Global logger class instance
37+
logger = Logger()
38+
39+
40+
LONG_REBOOT_PRESS_FILEPATH = '/var/run/hw-management/system/reset_long_pb'
41+
DEFAULT_USERS_FILEPATH = '/etc/sonic/default_users.json'
42+
43+
44+
class LocalUsersConfigurationReset(LocalUsersConfigurationResetBase):
45+
def should_trigger(self):
46+
'''
47+
The condition for triggering passwords reset is by checking if the
48+
reboot cause was a long reboot press.
49+
'''
50+
try:
51+
status = utils.read_int_from_file(LONG_REBOOT_PRESS_FILEPATH, raise_exception=True)
52+
return True if status == 1 else False
53+
except (ValueError, IOError) as e:
54+
logger.log_error(f"Failed to read long reboot press from {LONG_REBOOT_PRESS_FILEPATH} - {e}")
55+
return False
56+
57+
@staticmethod
58+
def reset_password(user, hashed_password, expire=False):
59+
'''
60+
This method is used to reset the user's password and expire it (optional)
61+
'''
62+
# Use 'chpasswd' shell command to change password
63+
subprocess.call(['echo', f'{user}:{hashed_password}', '|', 'sudo', 'chpasswd', '-e'])
64+
if expire:
65+
# Use 'passwd' shell command to expire password
66+
subprocess.call(['sudo', 'passwd', '-e', f'{user}'])
67+
68+
def start(self):
69+
'''
70+
The functionality defined is to restore original password and expire it for default local users.
71+
It is done by
72+
'''
73+
default_users = {}
74+
75+
# Fetch local users information from default_users
76+
with open(DEFAULT_USERS_FILEPATH) as f:
77+
default_users = json.load(f)
78+
79+
logger.log_info('Restoring default users\' passwords and expiring them')
80+
for user in default_users.keys():
81+
hashed_password = default_users.get(user, {}).get('password')
82+
if hashed_password:
83+
self.reset_password(user, hashed_password, expire=True)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#
2+
# Copyright (c) 2020-2024 NVIDIA CORPORATION & AFFILIATES.
3+
# Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
import os
18+
import pytest
19+
import subprocess
20+
import sys
21+
from mock import patch, mock_open
22+
23+
test_path = os.path.dirname(os.path.abspath(__file__))
24+
modules_path = os.path.dirname(test_path)
25+
sys.path.insert(0, modules_path)
26+
27+
from sonic_platform import utils
28+
from sonic_platform.local_users_passwords_reset import LocalUsersConfigurationReset
29+
30+
31+
DEFAULT_USERS_JSON_EXAMPLE_OUTPUT = '''
32+
{
33+
"admin": {
34+
"expire": "false",
35+
"password": "HASHED_PASSWORD_123"
36+
}
37+
}
38+
'''
39+
40+
41+
class TestLocalUsersConfigurationReset:
42+
@patch('sonic_platform.utils.read_int_from_file')
43+
def test_should_trigger_method(self, mock_read_int):
44+
'''
45+
Validate should_trigger() method
46+
'''
47+
local_users_reset_class = LocalUsersConfigurationReset()
48+
49+
mock_read_int.return_value = int(1)
50+
assert local_users_reset_class.should_trigger() == True
51+
mock_read_int.return_value = int(0)
52+
assert local_users_reset_class.should_trigger() == False
53+
mock_read_int.return_value = int(2)
54+
assert local_users_reset_class.should_trigger() == False
55+
mock_read_int.side_effect = ValueError()
56+
assert local_users_reset_class.should_trigger() == False
57+
58+
@patch('subprocess.call')
59+
@patch('sonic_platform.utils.read_int_from_file')
60+
@patch("builtins.open", new_callable=mock_open, read_data=DEFAULT_USERS_JSON_EXAMPLE_OUTPUT)
61+
def test_basic_flow_resetting_users_triggered(self, mock_read_int, mock_subproc_call):
62+
'''
63+
Test the basic flow of resetting local users when long button press is detected
64+
'''
65+
# Mock long reset button press
66+
mock_read_int.return_value = int(1)
67+
LocalUsersConfigurationReset().start()
68+
mock_subproc_call.assert_called_with(['echo', 'admin:HASHED_PASSWORD_123', '|', 'sudo', 'chpasswd', '-e'])
69+
mock_subproc_call.assert_called_with(['sudo', 'passwd', '-e', 'admin'])
70+
71+
@patch('subprocess.call')
72+
@patch('sonic_platform.utils.read_int_from_file')
73+
@patch("builtins.open", new_callable=mock_open, read_data=DEFAULT_USERS_JSON_OUTPUT)
74+
def test_basic_flow_resetting_users_not_triggered(self, mock_read_int, mock_subproc_call):
75+
'''
76+
Test the basic flow of resetting local users when long button press is not detected
77+
'''
78+
# Mock long reset button NOT pressed
79+
mock_read_int.return_value = int(0)
80+
LocalUsersConfigurationReset().start()
81+
mock_subproc_call.assert_not_called()

rules/config

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ DEFAULT_PASSWORD = YourPaSsWoRd
5151
# ENABLE_ZTP - installs Zero Touch Provisioning support.
5252
# ENABLE_ZTP = y
5353

54+
# ENABLE_LOCAL_USERS_PASSWORDS_RESET - enable local users' passwords reset during init on switch
55+
ENABLE_LOCAL_USERS_PASSWORDS_RESET ?= y
56+
5457
# INCLUDE_PDE - Enable platform development enviroment
5558
# INCLUDE_PDE = y
5659
# SHUTDOWN_BGP_ON_START - if set to y all bgp sessions will be in admin down state when

0 commit comments

Comments
 (0)