Skip to content

Commit

Permalink
Adding new test case for default password change after initial boot (#…
Browse files Browse the repository at this point in the history
…6863)

What is the motivation for this PR?
Validating default password change after initial boot for default user such as admin.

How did you do it?
1. taking a path to an image
2. manufacturing the switch to this image by uploading bin to ONIE and install it from ONIE
3. using Pexpect python module to communicate with the switch and validate expiring password message to appear after the first login.
4. suggesting a new password and then reconnecting to switch and validating that there is no expiring message to reappear
5. As part of clean-up we enforce the original password.

Supported testbed topology if it's a new test case?
any topology is supported.

Documentation
this test case is relevant for this HLD: sonic-net/SONiC#1077
  • Loading branch information
azmyali98 authored Mar 17, 2023
1 parent cd78a26 commit 359d84b
Show file tree
Hide file tree
Showing 6 changed files with 582 additions and 0 deletions.
Empty file.
104 changes: 104 additions & 0 deletions tests/platform_tests/test_first_time_boot_password_change/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import pytest
import logging
import time
import pexpect
from tests.platform_tests.test_first_time_boot_password_change.manufacture import manufacture
from tests.platform_tests.test_first_time_boot_password_change.default_consts import DefaultConsts


def pytest_addoption(parser):
parser.addoption("--feature_enabled", action="store", default='False', help="set to True if the feature is enabled")


class CurrentConfigurations:
'''
@summary: this class will act as a global database to save current configurations and changes the test made.
It will help us track the current state of the system,
and we will be used as part of cleanup fixtures.
'''
def __init__(self):
self.currentPassword = DefaultConsts.DEFAULT_PASSWORD # initial password


currentConfigurations = CurrentConfigurations()
logger = logging.getLogger(__name__)


@pytest.fixture(scope='module', autouse=True)
def dut_hostname(request):
'''
@summary: this function returns the hostname of the dut from the 'host-pattern'
'''
hostname = request.config.getoption('--host-pattern')
logger.info("Hostname is {}".format(hostname))
return hostname


@pytest.fixture(scope='module', autouse=True)
def is_feature_disabled(request):
'''
@summary: this fixture will be responsible for
skipping the test if the feature is disabled
'''
feature_enabled = request.config.getoption("feature_enabled")
if feature_enabled == 'False':
pytest.skip("Feature is disabled, will not run the test")


@pytest.fixture(scope='module', autouse=True)
def prepare_system_for_first_boot(request, dut_hostname):
'''
@summary: will manufacture the dut device to the given image in the parameter --base_image_list,
by installing the image given from ONIE. for detailed information read the documentation
of the manufacture script.
'''
base_image = request.config.getoption('base_image_list')
if not base_image:
pytest.skip("base_image_list param is empty")
manufacture(dut_hostname, base_image)


def change_password(dut_hostname, username, current_password, new_password):
'''
@summary: this function changes the password for the user given
:param dut_hostname: host name of the dut
:param dut_ip: device under test
:param username: user name to change the password for
:param current_password: current password
:param new_password: new password
'''
logger.info("Changing password for username:{} to password: {}".format(username, new_password))
try:
# create a new ssh connection
engine = pexpect.spawn(DefaultConsts.SSH_COMMAND.format(username) + dut_hostname, timeout=15)
# because of race condition
engine.delaybeforesend = 0.2
engine.delayafterclose = 0.5
engine.expect(DefaultConsts.PASSWORD_REGEX)
engine.sendline(current_password + '\r')
engine.expect(DefaultConsts.SONIC_PROMPT)
engine.sendline('sudo usermod -p $(openssl passwd -1 {}) {}'.format(new_password, username) + '\r')
engine.expect(DefaultConsts.SONIC_PROMPT)
logger.info("Sleeping for {} secs to apply password change".format(DefaultConsts.APPLY_CONFIGURATIONS))
time.sleep(DefaultConsts.APPLY_CONFIGURATIONS)
engine.sendline('exit')
engine.close()
except Exception as err:
logger.info('Got an exception while changing the password')
logger.info(str(err))


@pytest.fixture(scope='function', autouse=True)
def restore_original_password(dut_hostname):
'''
@summary: this function will restore the original password to the default one to allow
the next test to use default password to login to dut.
'''
yield
logger.info("Sleep {} secs for system stabilization".format(DefaultConsts.STABILIZATION_TIME))
time.sleep(DefaultConsts.STABILIZATION_TIME)
logger.info("Restore original password")
change_password(dut_hostname,
DefaultConsts.DEFAULT_USER,
currentConfigurations.currentPassword,
DefaultConsts.DEFAULT_PASSWORD)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'''
This file contains the default consts used by the scripts on the same folder:
manufactue.py and test_first_time_boot_password_change.py
'''


class DefaultConsts:
'''
@summary: a constants class used by the tests
'''
DEFAULT_USER = 'admin'
DEFAULT_PASSWORD = 'YourPaSsWoRd'
NEW_PASSWORD = 'Jg_GRK9BJB58s_5H'
ONIE_USER = 'root'
ONIE_PASSWORD = 'root'

# connection command
SSH_COMMAND = 'ssh -tt -q -o ControlMaster=auto -o ControlPersist=60s -o ' \
'ControlPath=/tmp/ansible-ssh-%h-%p-%r -o StrictHostKeyChecking=no ' \
'-o UserKnownHostsFile=/dev/null -o GSSAPIAuthentication=no ' \
'-o PubkeyAuthentication=no -p 22 -l {} '

SCP_COMMNAD = 'scp -o ControlMaster=auto ' \
'-o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r' \
' -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ' \
'GSSAPIAuthentication=no -o PubkeyAuthentication=no {} {}@{}:{}'

ONIE_INSTALL_PATH = 'platform_tests/test_first_time_boot_password_change/onie_install.sh'
# expired password message regex
PASSWORD_REGEX = 'assword'
SONIC_PROMPT = '$'
ONIE_PROMPT = '#'
DEFAULT_PROMPT = [SONIC_PROMPT, ONIE_PROMPT]
LONG_PERIOD = 30
APPLY_CONFIGURATIONS = 10
STABILIZATION_TIME = 60
SLEEP_AFTER_MANUFACTURE = 60
NEW_PASSWORD_REGEX = 'New password'
RETYPE_PASSWORD_REGEX = 'Retype new password'
# expired password message regex
EXPIRED_PASSWORD_MSG = 'You are required to change your password immediately'

# visual colors used for manufacture script
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
'''
This script will install a given image passed in the parameters
to the device using ONIE install mode.
Assumptions:
1. The system is up
2. Login username is 'admin' and default password: 'YourPaSsWoRd'
3. ONIE system with either no password to enter ONIE cli or 'root' password
4. Enough space to upload restore image to ONIE, otherwise it will fail
5. "onie_insall.sh" script in the same folder as this script
6. Existing image, will not check if the image path is existing, should be accessible without password!
Detailed logic of manufacture script:
1. Connect to dut
2. upload the "onie_install.sh" file in the same folder to dut
3. run the bash file, the script "onie_install.sh" is responsible for entering ONIE install mode
4. upload image to ONIE
5. install image using onie-nos-install
'''
import pexpect
import time
import logging
from tests.platform_tests.test_first_time_boot_password_change.default_consts import DefaultConsts


logger = logging.getLogger(__name__)


def print_log(msg, color=''):
'''
@summary: will print the msg to log, used to add color to messages,
since the manufacture script is long ~6 mins total
:param msg: msg to print to log
:param color: colot to print
'''
logger.info(color + msg + DefaultConsts.ENDC)


def ping_till_state(dut_ip, should_be_alive=True, timeout=300):
'''
@summary: this function will ping system till the desired
:param dut_ip: device under test ip address
:param should_be_alive: if True, will ping system till alive, if False will ping till down
:param timeout: fail if the desired state is not achieved
'''
# create an engine
localhost_engine = pexpect.spawn('sudo su', env={'TERM': 'dumb'})
localhost_engine.expect(['.*#', '.$'])
time_passed = 0
result = 'Fail'
while time_passed <= timeout:
print_log("Pinging system {} till {}".format(dut_ip, 'alive' if should_be_alive else 'down'))
localhost_engine.sendline('ping -c 1 ' + dut_ip)
response = localhost_engine.expect(['1 packets received', '0 packets received'])
if response == 0:
if should_be_alive:
result = 'Success'
break
else:
if not should_be_alive:
result = 'Success'
break

print_log("Sleeping 2 secs between pings")
time.sleep(2)
time_passed += 2

if result == 'Fail':
fail_msg = "Expected system to be {} after timeout of {} but the system was {}".format(
'alive' if should_be_alive else 'down',
timeout,
'down' if should_be_alive else 'alive'
)
print_log(fail_msg)
localhost_engine.close()


def ping_till_alive(dut_ip, timeout=300):
'''
@summary: this function will ping system till alive
:param dut_ip: device under test ip address
'''
ping_till_state(dut_ip, should_be_alive=True, timeout=timeout)


def ping_till_down(dut_ip, timeout=300):
'''
@summary: this function will ping system till down
:param dut_ip: device under test ip address
'''
ping_till_state(dut_ip, should_be_alive=False, timeout=timeout)


def create_engine(dut_ip, username, password, timeout=30):
'''
@summary: this command will create an ssh engine to run command
on the device under test
:param dut_ip: device under test ip address
:param username: user name to login
:param password: password for username
:param timeout: default timeout for engine
'''
print_log("Creating engine for {} with username: {} and password: {}".format(dut_ip, username, password))
child = pexpect.spawn(DefaultConsts.SSH_COMMAND.format(username) + dut_ip, env={'TERM': 'dumb'}, timeout=timeout)
index = child.expect([DefaultConsts.PASSWORD_REGEX,
DefaultConsts.DEFAULT_PROMPT[0],
DefaultConsts.DEFAULT_PROMPT[1]])
if index == 0:
child.sendline(password + '\r')

print_log("Engine created successfully")
return child


def upload_file_to_dut(dut_ip, filename, destination, username, password, timeout=30):
'''
@summary: this function will upload the given file to dut under destination folder
:param dut_ip: device under test
:param filename: path to filenmae
:param username: username of the device
:param password: password to username
:param timeout: timeout
'''
print_log('Uploading file {} to dut {} under \'{}\' dir'.format(filename, dut_ip, destination))
if timeout > DefaultConsts.LONG_PERIOD:
print_log('Please be patient this may take some time')
cmd = DefaultConsts.SCP_COMMNAD.format(filename, username, dut_ip, destination)
child = pexpect.spawn(cmd, timeout=timeout)
# sometimes the system requires password to login into, we need to consider this case
index = child.expect(["100%",
DefaultConsts.PASSWORD_REGEX])
if index == 0:
print_log('Done Uploading file - 100%', DefaultConsts.OKGREEN)
return
# enter password
child.sendline(password + '\r')
child.expect(['100%'])
print_log('Done Uploading file - 100%', DefaultConsts.OKGREEN)


def enter_onie_install_mode(dut_ip):
'''
@summary: this function will upload the "onie_install.sh" bash script under '/tmp' folder on dut.
The script is found in the same folder of this script. The script is executed from the dut.
The script "onie_install.sh" is responsible for loading ONIE install mode after reboot.
For more info please read the documentation in the bash script and its usage.
:param dut_ip: device under test ip address
'''
print_log("Entering ONIE install mode by running \"{}\" bash script on DUT".format(
DefaultConsts.ONIE_INSTALL_PATH.split('/')[-1]),
DefaultConsts.WARNING + DefaultConsts.BOLD)

upload_file_to_dut(dut_ip, DefaultConsts.ONIE_INSTALL_PATH, '/tmp',
DefaultConsts.DEFAULT_USER,
DefaultConsts.DEFAULT_PASSWORD)
# create ssh connection device
sonic_engine = create_engine(dut_ip, DefaultConsts.DEFAULT_USER, DefaultConsts.DEFAULT_PASSWORD)
sonic_engine.sendline('sudo su')
sonic_engine.expect(DefaultConsts.SONIC_PROMPT)
sonic_engine.sendline('cd /tmp')
sonic_engine.expect(DefaultConsts.SONIC_PROMPT)
print_log("Validating file \"{}\" existence".format(DefaultConsts.ONIE_INSTALL_PATH.split('/')[-1]))
# validate the file is there
sonic_engine.sendline('ls')
sonic_engine.expect('{}'.format(DefaultConsts.ONIE_INSTALL_PATH.split('/')[-1]))
# # change permissions
print_log("Executing the bash script uploaded")
sonic_engine.sendline('sudo chmod +777 onie_install.sh')
sonic_engine.expect(DefaultConsts.SONIC_PROMPT)
sonic_engine.sendline('sudo ./onie_install.sh install')
sonic_engine.expect('Reboot will be done after 3 sec')
# # close session, the system will perform reboot
ping_till_down(dut_ip)
print_log("System is Down!", DefaultConsts.BOLD + DefaultConsts.OKGREEN)
sonic_engine.close()


def install_image_from_onie(dut_ip, restore_image_path):
'''
@summary: this function will upload the image given to ONIE and perform
install to the image using "onie-nos-install"
:param dut_ip: device under test ip address
:param restore_image_path: path to restore image should be in the format /../../../your_image_name.bin
'''
ping_till_alive(dut_ip)
print_log("System is UP!", DefaultConsts.BOLD + DefaultConsts.OKGREEN)
upload_file_to_dut(dut_ip, restore_image_path, '/', DefaultConsts.ONIE_USER, DefaultConsts.ONIE_PASSWORD,
timeout=420)

restore_image_name = restore_image_path.split('/')[-1]
print_log('restore image name is {}'.format(restore_image_name))
# SSH to ONIE
child = create_engine(dut_ip, DefaultConsts.ONIE_USER, DefaultConsts.ONIE_PASSWORD)
print_log("Install the image from ONIE")
child.sendline('cd /')
child.expect(DefaultConsts.ONIE_PROMPT)
child.sendline('onie-stop')
child.expect(DefaultConsts.ONIE_PROMPT)
child.sendline('onie-nos-install {}'.format(restore_image_name) + '\r')
print_log("Ping system till down")
ping_till_down(dut_ip)
print_log("Ping system till alive")
ping_till_alive(dut_ip)
child.close()


def manufacture(dut_ip, restore_image_path):
'''
@summary: will remove the installed image and intsall the image given in the restore_image_path
Assumptions:
1. The system is up
2. Login username is 'admin' and default password: 'YourPaSsWoRd'
3. ONIE system with either no password to enter ONIE cli or 'root' password
4. Enough space to upload restore image to ONIE, otherwise it will fail, and will leave system in ONIE mode!
5. "onie_insall.sh" script in the same folder as this script,
under "tests/platform_tests/test_first_time_boot_password_change"
6. Existing image, will not check if the image path is existing, should be accessible without password!
Detailed logic of manufacture script:
1. Connect to dut
2. upload the "onie_install.sh" file in the same folder to dut
3. run the bash file, the script "onie_install.sh" is responsible for entering ONIE install mode
4. upload image to ONIE
5. install image using onie-nos-install
:param dut_ip: device to manufacture
:param restore_image_path: path to the image
'''
# create engine for the localhost running this script
print_log("Manufacture started", DefaultConsts.OKGREEN + DefaultConsts.BOLD)
# perform manufacture
enter_onie_install_mode(dut_ip)
install_image_from_onie(dut_ip, restore_image_path)
print_log("Sleeping for {} secs to stabilize system after reboot".format(DefaultConsts.SLEEP_AFTER_MANUFACTURE))
time.sleep(DefaultConsts.SLEEP_AFTER_MANUFACTURE)
print_log("Manufacture is completed - SUCCESS", DefaultConsts.OKGREEN + DefaultConsts.BOLD)
Loading

0 comments on commit 359d84b

Please sign in to comment.