From b74c3935fe51314ddb3cdf22e3813eb36f3d7b0c Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Thu, 2 Mar 2023 18:39:40 +0800 Subject: [PATCH 1/4] Add support of using lab tacacs server by default The current default configuration of DUTs does not support using lab tacacs server for authentication. This change added a new variable `tacacs_enabled_by_default` in group_vars file to control enablement of lab tacacs server. The ansible playbook for deploy minigraph was updated to support configuring lab tacacs server based on value of this variable. The tacacs test scripts are updated to support recover the lab tacacs server in a clean way after test is done. Signed-off-by: Xin Wang --- ansible/config_sonic_basedon_testbed.yml | 16 ++++ ansible/group_vars/lab/lab.yml | 10 ++- tests/common/utilities.py | 4 +- tests/tacacs/conftest.py | 13 +-- tests/tacacs/templates/del_tacacs_keys.json | 6 -- tests/tacacs/utils.py | 94 +++++++++++++++------ 6 files changed, 103 insertions(+), 40 deletions(-) delete mode 100644 tests/tacacs/templates/del_tacacs_keys.json diff --git a/ansible/config_sonic_basedon_testbed.yml b/ansible/config_sonic_basedon_testbed.yml index 547eba8e1bd..a7aa797e3eb 100644 --- a/ansible/config_sonic_basedon_testbed.yml +++ b/ansible/config_sonic_basedon_testbed.yml @@ -560,6 +560,22 @@ become: true shell: config bgp startup all + - name: Configure TACACS + become: true + shell: "{{ tacacs_config_cmd }}" + loop: + - config tacacs passkey {{ tacacs_passkey }} + - config tacacs authtype pap + - config tacacs timeout 5 + - config aaa authentication login tacacs+ + - config aaa authentication failthrough default + - config aaa authorization tacacs+ + - config aaa accounting tacacs+ + loop_control: + loop_var: tacacs_config_cmd + ignore_errors: true + when: tacacs_enabled_by_default is defined and tacacs_enabled_by_default|bool == true + - name: execute configlet application script, which applies configlets in strict order. become: true shell: bash "/etc/sonic/apply_clet.sh" diff --git a/ansible/group_vars/lab/lab.yml b/ansible/group_vars/lab/lab.yml index 1605880463b..c02f6970ed8 100644 --- a/ansible/group_vars/lab/lab.yml +++ b/ansible/group_vars/lab/lab.yml @@ -12,7 +12,7 @@ syslog_servers: ['10.0.0.5', '10.0.0.6'] dns_servers: ['10.0.0.5', '10.0.0.6'] # forced_mgmt_routes -forced_mgmt_routes: ['172.17.0.1'] +forced_mgmt_routes: ['172.17.0.1/24'] # ErspanDestinationIpv4 erspan_dest: ['10.0.0.7'] @@ -21,13 +21,17 @@ radius_servers: [] radius_passkey: testing123 -tacacs_servers: ['10.0.20.9', '10.0.20.8'] - +# It can be a real lab tacacs server. +tacacs_servers: ['172.17.0.6'] tacacs_passkey: testing123 # tacacs grous tacacs_group: 'testlab' +# Determine whether enable tacacs authentication during deploy-minigraph. If false, use local authentication. +# If yes, authenticate using servers configured in `tacacs_servers` +tacacs_enabled_by_default: false + # snmp servers snmp_servers: ['10.0.0.9'] diff --git a/tests/common/utilities.py b/tests/common/utilities.py index 36d3c941ce7..19dbe954969 100644 --- a/tests/common/utilities.py +++ b/tests/common/utilities.py @@ -320,8 +320,10 @@ def get_host_visible_vars(inv_files, hostname): The variable could be defined in host_vars or in group_vars that the host belongs to. Args: - inv_files (list or string): List of inventory file pathes, or string of a single inventory file path. In tests, + inv_files (list or string): List of inventory file paths, or string of a single inventory file path. In tests, it can be get from request.config.getoption("ansible_inventory"). + MUST use the inventory file under the ansible folder, otherwise host_vars and group_vars would not be + visible. hostname (string): Hostname Returns: diff --git a/tests/tacacs/conftest.py b/tests/tacacs/conftest.py index c95399b27c3..165f3d52056 100644 --- a/tests/tacacs/conftest.py +++ b/tests/tacacs/conftest.py @@ -5,7 +5,7 @@ from .utils import setup_tacacs_client, setup_tacacs_server, cleanup_tacacs, restore_tacacs_servers logger = logging.getLogger(__name__) -TACACS_CREDS_FILE='tacacs_creds.yaml' +TACACS_CREDS_FILE = 'tacacs_creds.yaml' @pytest.fixture(scope="module") @@ -15,21 +15,23 @@ def tacacs_creds(creds_all_duts): creds_all_duts.update(hardcoded_creds) return creds_all_duts + @pytest.fixture(scope="module") -def check_tacacs(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds): +def check_tacacs(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, request): logger.info('tacacs_creds: {}'.format(str(tacacs_creds))) duthost = duthosts[enum_rand_one_per_hwsku_hostname] tacacs_server_ip = ptfhost.mgmt_ip - default_tacacs_servers = setup_tacacs_client(duthost, tacacs_creds, tacacs_server_ip) + setup_tacacs_client(duthost, tacacs_creds, tacacs_server_ip) setup_tacacs_server(ptfhost, tacacs_creds, duthost) yield cleanup_tacacs(ptfhost, tacacs_creds, duthost) - restore_tacacs_servers(duthost, default_tacacs_servers, tacacs_server_ip) + restore_tacacs_servers(duthost, request) + @pytest.fixture(scope="module") -def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds): +def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, request): duthost = duthosts[enum_rand_one_per_hwsku_hostname] ptfhost_vars = ptfhost.host.options['inventory_manager'].get_host(ptfhost.hostname).vars if 'ansible_hostv6' not in ptfhost_vars: @@ -41,3 +43,4 @@ def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_ yield cleanup_tacacs(ptfhost, tacacs_creds, duthost) + restore_tacacs_servers(duthost, request) diff --git a/tests/tacacs/templates/del_tacacs_keys.json b/tests/tacacs/templates/del_tacacs_keys.json deleted file mode 100644 index 4ac970dbbd3..00000000000 --- a/tests/tacacs/templates/del_tacacs_keys.json +++ /dev/null @@ -1,6 +0,0 @@ -[{ - "AAA":{} -}, -{ - "TACPLUS":{} -}] diff --git a/tests/tacacs/utils.py b/tests/tacacs/utils.py index 5d7114cf7f3..35d03cc3aa8 100644 --- a/tests/tacacs/utils.py +++ b/tests/tacacs/utils.py @@ -4,21 +4,24 @@ from tests.common.errors import RunAnsibleModuleFail from tests.common.utilities import wait_until, check_skip_release +from tests.common.utilities import get_inventory_files, get_host_visible_vars from tests.common.helpers.assertions import pytest_assert -from tests.common.errors import RunAnsibleModuleFail logger = logging.getLogger(__name__) + # per-command authorization and accounting feature not avaliable in following versions per_command_check_skip_versions = ["201811", "201911", "202012", "202106"] + def check_output(output, exp_val1, exp_val2): pytest_assert(not output['failed'], output['stderr']) - for l in output['stdout_lines']: - fds = l.split(':') + for line in output['stdout_lines']: + fds = line.split(':') if fds[0] == exp_val1: pytest_assert(fds[4] == exp_val2) + def check_all_services_status(ptfhost): res = ptfhost.command("service --status-all") logger.info(res["stdout_lines"]) @@ -28,10 +31,12 @@ def start_tacacs_server(ptfhost): ptfhost.command("service tacacs_plus restart", module_ignore_errors=True) return "tacacs+ running" in ptfhost.command("service tacacs_plus status", module_ignore_errors=True)["stdout_lines"] + def stop_tacacs_server(ptfhost): ptfhost.service(name="tacacs_plus", state="stopped") check_all_services_status(ptfhost) + def setup_local_user(duthost, tacacs_creds): try: duthost.shell("sudo deluser {}".format(tacacs_creds['local_user'])) @@ -39,7 +44,8 @@ def setup_local_user(duthost, tacacs_creds): logger.info("local user not exist") duthost.shell("sudo useradd {}".format(tacacs_creds['local_user'])) - duthost.shell('sudo echo "{}:{}" | chpasswd'.format(tacacs_creds['local_user'],tacacs_creds['local_user_passwd'])) + duthost.shell('sudo echo "{}:{}" | chpasswd'.format(tacacs_creds['local_user'], tacacs_creds['local_user_passwd'])) + def setup_tacacs_client(duthost, tacacs_creds, tacacs_server_ip): """setup tacacs client""" @@ -68,35 +74,59 @@ def setup_tacacs_client(duthost, tacacs_creds, tacacs_server_ip): setup_local_user(duthost, tacacs_creds) return default_tacacs_servers -def restore_tacacs_servers(duthost, default_tacacs_servers, tacacs_server_ip): - duthost.shell("sudo config tacacs delete %s" % tacacs_server_ip) - for tacacs_server in default_tacacs_servers: + +def restore_tacacs_servers(duthost, request): + # Restore the TACACS plus server in config_db.json + config_facts = duthost.config_facts(host=duthost.hostname, source="persistent")["ansible_facts"] + for tacacs_server in config_facts.get("TACPLUS_SERVER", {}): duthost.shell("sudo config tacacs add %s" % tacacs_server) -def fix_symbolic_link_in_config(duthost, ptfhost, symbolic_link_path, path_to_be_fix = None): + # Follow the same logic in "config_sonic_basedon_testbed.yml" to configure tacacs and aaa + inv_files = get_inventory_files(request) + dut_vars = get_host_visible_vars(inv_files, duthost.hostname) + tacacs_enabled_by_default = dut_vars.get("tacacs_enabled_by_default", False) + if tacacs_enabled_by_default: + tacacs_passkey = dut_vars.get("tacacs_passkey", "") + cmds = [ + "config tacacs passkey %s" % tacacs_passkey, + "config tacacs authtype pap", + "config tacacs timeout 5", + "config aaa authentication login tacacs+", + "config aaa authentication failthrough default", + "config aaa authorization tacacs+", + "config aaa accounting tacacs+" + ] + duthost.shell_cmds(cmds=cmds, module_ignore_errors=True) + + +def fix_symbolic_link_in_config(duthost, ptfhost, symbolic_link_path, path_to_be_fix=None): """ Fix symbolic link in tacacs config - Because tac_plus server not support regex in command name, and SONiC will send full path to tacacs server side for authorization, so the 'python' and 'ld' path in tac_plus config file need fix. + Because tac_plus server not support regex in command name, and SONiC will send full path to tacacs server side + for authorization, so the 'python' and 'ld' path in tac_plus config file need fix. """ read_link_command = "readlink -f {0}".format(symbolic_link_path) target_path = duthost.shell(read_link_command)['stdout'] # Escape path string, will use it as regex in sed command. - + link_path_regex = re.escape(symbolic_link_path) - if path_to_be_fix != None: + if path_to_be_fix is not None: link_path_regex = re.escape(path_to_be_fix) target_path_regex = re.escape(target_path) ptfhost.shell("sed -i 's|{0}|{1}|g' /etc/tacacs+/tac_plus.conf".format(link_path_regex, target_path_regex)) + def get_ld_path(duthost): """ Fix symbolic link in tacacs config - Because tac_plus server not support regex in command name, and SONiC will send full path to tacacs server side for authorization, so the 'python' and 'ld' path in tac_plus config file need fix. + Because tac_plus server not support regex in command name, and SONiC will send full path to tacacs server side + for authorization, so the 'python' and 'ld' path in tac_plus config file need fix. """ - find_ld_command = "find /lib/ -type f,l -regex '\/lib\/.*-linux-.*/ld-linux-.*\.so\.[0-9]*'" + find_ld_command = "find /lib/ -type f,l -regex '\/lib\/.*-linux-.*/ld-linux-.*\.so\.[0-9]*'" # noqa W605 return duthost.shell(find_ld_command)['stdout'] + def fix_ld_path_in_config(duthost, ptfhost): """ Fix ld path in tacacs config @@ -105,6 +135,7 @@ def fix_ld_path_in_config(duthost, ptfhost): if not ld_symbolic_link_path: fix_symbolic_link_in_config(duthost, ptfhost, ld_symbolic_link_path, "/lib/arch-linux-abi/ld-linux-arch.so") + def setup_tacacs_server(ptfhost, tacacs_creds, duthost): """setup tacacs server""" @@ -115,7 +146,9 @@ def setup_tacacs_server(ptfhost, tacacs_creds, duthost): 'tacacs_ro_user': tacacs_creds['tacacs_ro_user'], 'tacacs_ro_user_passwd': crypt.crypt(tacacs_creds['tacacs_ro_user_passwd'], 'abc'), 'tacacs_authorization_user': tacacs_creds['tacacs_authorization_user'], - 'tacacs_authorization_user_passwd': crypt.crypt(tacacs_creds['tacacs_authorization_user_passwd'], 'abc'), + 'tacacs_authorization_user_passwd': crypt.crypt( + tacacs_creds['tacacs_authorization_user_passwd'], + 'abc'), 'tacacs_jit_user': tacacs_creds['tacacs_jit_user'], 'tacacs_jit_user_passwd': crypt.crypt(tacacs_creds['tacacs_jit_user_passwd'], 'abc'), 'tacacs_jit_user_membership': tacacs_creds['tacacs_jit_user_membership']} @@ -129,7 +162,11 @@ def setup_tacacs_server(ptfhost, tacacs_creds, duthost): # Find ld lib symbolic link target, and fix the tac_plus config file fix_ld_path_in_config(duthost, ptfhost) - ptfhost.lineinfile(path="/etc/default/tacacs+", line="DAEMON_OPTS=\"-d 10 -l /var/log/tac_plus.log -C /etc/tacacs+/tac_plus.conf\"", regexp='^DAEMON_OPTS=.*') + ptfhost.lineinfile( + path="/etc/default/tacacs+", + line="DAEMON_OPTS=\"-d 10 -l /var/log/tac_plus.log -C /etc/tacacs+/tac_plus.conf\"", + regexp='^DAEMON_OPTS=.*' + ) check_all_services_status(ptfhost) # FIXME: This is a short term mitigation, we need to figure out why \nthe tacacs+ server does not start @@ -144,26 +181,33 @@ def cleanup_tacacs(ptfhost, tacacs_creds, duthost): # reset tacacs client configuration remove_all_tacacs_server(duthost) - duthost.shell("sudo config tacacs default passkey") - duthost.shell("sudo config aaa authentication login default") - duthost.shell("sudo config aaa authentication failthrough default") + cmds = [ + "config tacacs default passkey", + "config aaa authentication login default", + "config aaa authentication failthrough default" + ] + duthost.shell_cmds(cmds=cmds) (skip, _) = check_skip_release(duthost, per_command_check_skip_versions) if not skip: duthost.shell("sudo config aaa authorization local") duthost.shell("sudo config aaa accounting disable") - duthost.user(name=tacacs_creds['tacacs_ro_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True) - duthost.user(name=tacacs_creds['tacacs_rw_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True) - duthost.user(name=tacacs_creds['tacacs_jit_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True) + duthost.user( + name=tacacs_creds['tacacs_ro_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True + ) + duthost.user( + name=tacacs_creds['tacacs_rw_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True + ) + duthost.user( + name=tacacs_creds['tacacs_jit_user'], state='absent', remove='yes', force='yes', module_ignore_errors=True + ) - duthost.copy(src="./tacacs/templates/del_tacacs_keys.json", dest='/tmp/del_tacacs_keys.json') - duthost.shell("configlet -d -j {}".format("/tmp/del_tacacs_keys.json")) def remove_all_tacacs_server(duthost): # use grep command to extract tacacs server address from tacacs config - find_server_command = 'show tacacs | grep -Po "TACPLUS_SERVER address \K.*"' - server_list = duthost.shell(find_server_command, module_ignore_errors=True)['stdout'] + find_server_command = 'show tacacs | grep -Po "TACPLUS_SERVER address \K.*"' # noqa W605 + server_list = duthost.shell(find_server_command, module_ignore_errors=True)['stdout_lines'] for tacacs_server in server_list: tacacs_server = tacacs_server.rstrip() if tacacs_server: From 739fc110deef3ae731cc9302d96f64df1b595d0f Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 3 Mar 2023 09:18:26 +0800 Subject: [PATCH 2/4] Use a different method to recover TACACS configuration --- ansible/config_sonic_basedon_testbed.yml | 4 -- tests/tacacs/utils.py | 58 +++++++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/ansible/config_sonic_basedon_testbed.yml b/ansible/config_sonic_basedon_testbed.yml index a7aa797e3eb..985e590d4ee 100644 --- a/ansible/config_sonic_basedon_testbed.yml +++ b/ansible/config_sonic_basedon_testbed.yml @@ -566,11 +566,7 @@ loop: - config tacacs passkey {{ tacacs_passkey }} - config tacacs authtype pap - - config tacacs timeout 5 - config aaa authentication login tacacs+ - - config aaa authentication failthrough default - - config aaa authorization tacacs+ - - config aaa accounting tacacs+ loop_control: loop_var: tacacs_config_cmd ignore_errors: true diff --git a/tests/tacacs/utils.py b/tests/tacacs/utils.py index 35d03cc3aa8..7b5ea1a6e2e 100644 --- a/tests/tacacs/utils.py +++ b/tests/tacacs/utils.py @@ -4,7 +4,6 @@ from tests.common.errors import RunAnsibleModuleFail from tests.common.utilities import wait_until, check_skip_release -from tests.common.utilities import get_inventory_files, get_host_visible_vars from tests.common.helpers.assertions import pytest_assert logger = logging.getLogger(__name__) @@ -81,22 +80,47 @@ def restore_tacacs_servers(duthost, request): for tacacs_server in config_facts.get("TACPLUS_SERVER", {}): duthost.shell("sudo config tacacs add %s" % tacacs_server) - # Follow the same logic in "config_sonic_basedon_testbed.yml" to configure tacacs and aaa - inv_files = get_inventory_files(request) - dut_vars = get_host_visible_vars(inv_files, duthost.hostname) - tacacs_enabled_by_default = dut_vars.get("tacacs_enabled_by_default", False) - if tacacs_enabled_by_default: - tacacs_passkey = dut_vars.get("tacacs_passkey", "") - cmds = [ - "config tacacs passkey %s" % tacacs_passkey, - "config tacacs authtype pap", - "config tacacs timeout 5", - "config aaa authentication login tacacs+", - "config aaa authentication failthrough default", - "config aaa authorization tacacs+", - "config aaa accounting tacacs+" - ] - duthost.shell_cmds(cmds=cmds, module_ignore_errors=True) + cmds = [] + aaa_config = config_facts.get("AAA", {}) + if aaa_config: + cfg = aaa_config.get("authentication", {}).get("login", "") + if cfg: + cmds.append("config aaa authentication login %s" % cfg) + + cfg = aaa_config.get("authentication", {}).get("failthrough", "") + if cfg.lower() == "true": + cmds.append("config aaa authentication failthrough enable") + elif cfg.lower() == "false": + cmds.append("config aaa authentication failthrough disable") + + cfg = aaa_config.get("authorization", {}).get("login", "") + if cfg: + cmds.append("config aaa authorization %s" % cfg) + + cfg = aaa_config.get("accounting", {}).get("login", "") + if cfg: + cmds.append("config aaa accounting %s" % cfg) + + tacplus_config = config_facts.get("TACPLUS", {}) + if tacplus_config: + cfg = tacplus_config.get("global", {}).get("auth_type", "") + if cfg: + cmds.append("config tacacs authtype %s" % cfg) + + cfg = tacplus_config.get("global", {}).get("passkey", "") + if cfg: + cmds.append("config tacacs passkey %s" % cfg) + + cfg = tacplus_config.get("global", {}).get("timeout", "") + if cfg: + cmds.append("config tacacs timeout %s" % cfg) + + # Cleanup AAA and TACPLUS config + duthost.copy(src="./tacacs/templates/del_tacacs_config.json", dest='/tmp/del_tacacs_config.json') + duthost.shell("configlet -d -j {}".format("/tmp/del_tacacs_config.json")) + + # Restore AAA and TACPLUS config + duthost.shell_cmds(cmds=cmds) def fix_symbolic_link_in_config(duthost, ptfhost, symbolic_link_path, path_to_be_fix=None): From 0e8b13bd047045b72ea7c3c9cf13a5494c892609 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 3 Mar 2023 09:22:21 +0800 Subject: [PATCH 3/4] Add configlet for cleanup tacacs config --- tests/tacacs/templates/del_tacacs_config.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/tacacs/templates/del_tacacs_config.json diff --git a/tests/tacacs/templates/del_tacacs_config.json b/tests/tacacs/templates/del_tacacs_config.json new file mode 100644 index 00000000000..4ac970dbbd3 --- /dev/null +++ b/tests/tacacs/templates/del_tacacs_config.json @@ -0,0 +1,6 @@ +[{ + "AAA":{} +}, +{ + "TACPLUS":{} +}] From 1ad87996e04cb2b41febe0c351f96f73d0cce582 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 3 Mar 2023 09:56:21 +0800 Subject: [PATCH 4/4] Remove unused request fixture --- tests/tacacs/conftest.py | 8 ++++---- tests/tacacs/utils.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/tacacs/conftest.py b/tests/tacacs/conftest.py index 165f3d52056..8fcb00de99c 100644 --- a/tests/tacacs/conftest.py +++ b/tests/tacacs/conftest.py @@ -17,7 +17,7 @@ def tacacs_creds(creds_all_duts): @pytest.fixture(scope="module") -def check_tacacs(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, request): +def check_tacacs(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds): logger.info('tacacs_creds: {}'.format(str(tacacs_creds))) duthost = duthosts[enum_rand_one_per_hwsku_hostname] tacacs_server_ip = ptfhost.mgmt_ip @@ -27,11 +27,11 @@ def check_tacacs(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_cre yield cleanup_tacacs(ptfhost, tacacs_creds, duthost) - restore_tacacs_servers(duthost, request) + restore_tacacs_servers(duthost) @pytest.fixture(scope="module") -def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds, request): +def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_creds): duthost = duthosts[enum_rand_one_per_hwsku_hostname] ptfhost_vars = ptfhost.host.options['inventory_manager'].get_host(ptfhost.hostname).vars if 'ansible_hostv6' not in ptfhost_vars: @@ -43,4 +43,4 @@ def check_tacacs_v6(ptfhost, duthosts, enum_rand_one_per_hwsku_hostname, tacacs_ yield cleanup_tacacs(ptfhost, tacacs_creds, duthost) - restore_tacacs_servers(duthost, request) + restore_tacacs_servers(duthost) diff --git a/tests/tacacs/utils.py b/tests/tacacs/utils.py index 7b5ea1a6e2e..6008f88d7c7 100644 --- a/tests/tacacs/utils.py +++ b/tests/tacacs/utils.py @@ -74,7 +74,7 @@ def setup_tacacs_client(duthost, tacacs_creds, tacacs_server_ip): return default_tacacs_servers -def restore_tacacs_servers(duthost, request): +def restore_tacacs_servers(duthost): # Restore the TACACS plus server in config_db.json config_facts = duthost.config_facts(host=duthost.hostname, source="persistent")["ansible_facts"] for tacacs_server in config_facts.get("TACPLUS_SERVER", {}):