diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index d95071d1a..9cf6a653a 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -1,192 +1,196 @@
 # Copyright (C) 2021-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 import unittest
 import paramiko
 import pprint
 
 from time import sleep
 from time import time
 from typing import Type
 
 from vyos.configsession import ConfigSession
 from vyos.configsession import ConfigSessionError
 from vyos import ConfigError
 from vyos.defaults import commit_lock
 from vyos.utils.process import cmd
 from vyos.utils.process import run
+from vyos.utils.process import process_named_running
 
 save_config = '/tmp/vyos-smoketest-save'
 
 # This class acts as shim between individual Smoketests developed for VyOS and
 # the Python UnitTest framework. Before every test is loaded, we dump the current
 # system configuration and reload it after the test - despite the test results.
 #
 # Using this approach we can not render a live system useless while running any
 # kind of smoketest. In addition it adds debug capabilities like printing the
 # command used to execute the test.
 class VyOSUnitTestSHIM:
     class TestCase(unittest.TestCase):
         # if enabled in derived class, print out each and every set/del command
         # on the CLI. This is usefull to grap all the commands required to
         # trigger the certain failure condition.
         # Use "self.debug = True" in derived classes setUp() method
         debug = False
         commit_guard = time()
         @classmethod
         def setUpClass(cls):
             cls._session = ConfigSession(os.getpid())
             cls._session.save_config(save_config)
             if os.path.exists('/tmp/vyos.smoketest.debug'):
                 cls.debug = True
             pass
 
         @classmethod
         def tearDownClass(cls):
             # discard any pending changes which might caused a messed up config
             cls._session.discard()
             # ... and restore the initial state
             cls._session.migrate_and_load_config(save_config)
 
             try:
                 cls._session.commit()
             except (ConfigError, ConfigSessionError):
                 cls._session.discard()
                 cls.fail(cls)
 
         def cli_set(self, config):
             if self.debug:
                 print('set ' + ' '.join(config))
             self._session.set(config)
 
         def cli_delete(self, config):
             if self.debug:
                 print('del ' + ' '.join(config))
             self._session.delete(config)
 
         def cli_discard(self):
             if self.debug:
                 print('DISCARD')
             self._session.discard()
 
         def cli_commit(self):
             if self.debug:
                 print('commit')
             self._session.commit()
             # during a commit there is a process opening commit_lock, and run() returns 0
             while run(f'sudo lsof -nP {commit_lock}') == 0:
                 sleep(0.250)
+            # wait for FRR reload to be complete
+            while process_named_running('frr-reload.py'):
+                sleep(0.250)
             # reset getFRRconfig() guard timer
             self.commit_guard = time()
 
         def op_mode(self, path : list) -> None:
             """
             Execute OP-mode command and return stdout
             """
             if self.debug:
                 print('commit')
             path = ' '.join(path)
             out = cmd(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {path}')
             if self.debug:
                 print(f'\n\ncommand "{path}" returned:\n')
                 pprint.pprint(out)
             return out
 
         def getFRRconfig(self, string=None, end='$', endsection='^!', daemon='', guard_time=10, empty_retry=0):
             """ Retrieve current "running configuration" from FRR """
             # Sometimes FRR needs some time after reloading the configuration to
             # appear in vtysh. This is a workaround addiung a 10 second guard timer
             # between the last cli_commit() and the first read of FRR config via vtysh
             while (time() - self.commit_guard) < guard_time:
                 sleep(0.250) # wait 250 milliseconds
             command = f'vtysh -c "show run {daemon} no-header"'
             if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"'
             out = cmd(command)
             if self.debug:
                 print(f'\n\ncommand "{command}" returned:\n')
                 pprint.pprint(out)
             if empty_retry:
                 retry_count = 0
                 while not out and retry_count < empty_retry:
                     if self.debug and retry_count % 10 == 0:
                         print(f"Attempt {retry_count}: FRR config is still empty. Retrying...")
                     retry_count += 1
                     sleep(1)
                     out = cmd(command)
                 if not out:
                     print(f'FRR configuration still empty after {empty_retry} retires!')
             return out
 
         @staticmethod
         def ssh_send_cmd(command, username, password, hostname='localhost'):
             """ SSH command execution helper """
             # Try to login via SSH
             ssh_client = paramiko.SSHClient()
             ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
             ssh_client.connect(hostname=hostname, username=username, password=password)
             _, stdout, stderr = ssh_client.exec_command(command)
             output = stdout.read().decode().strip()
             error = stderr.read().decode().strip()
             ssh_client.close()
             return output, error
 
         # Verify nftables output
         def verify_nftables(self, nftables_search, table, inverse=False, args=''):
             nftables_output = cmd(f'sudo nft {args} list table {table}')
 
             for search in nftables_search:
                 matched = False
                 for line in nftables_output.split("\n"):
                     if all(item in line for item in search):
                         matched = True
                         break
                 self.assertTrue(not matched if inverse else matched, msg=search)
 
         def verify_nftables_chain(self, nftables_search, table, chain, inverse=False, args=''):
             nftables_output = cmd(f'sudo nft {args} list chain {table} {chain}')
 
             for search in nftables_search:
                 matched = False
                 for line in nftables_output.split("\n"):
                     if all(item in line for item in search):
                         matched = True
                         break
                 self.assertTrue(not matched if inverse else matched, msg=search)
 
         # Verify ip rule output
         def verify_rules(self, rules_search, inverse=False, addr_family='inet'):
             rule_output = cmd(f'ip -family {addr_family} rule show')
 
             for search in rules_search:
                 matched = False
                 for line in rule_output.split("\n"):
                     if all(item in line for item in search):
                         matched = True
                         break
                 self.assertTrue(not matched if inverse else matched, msg=search)
 
 # standard construction; typing suggestion: https://stackoverflow.com/a/70292317
 def ignore_warning(warning: Type[Warning]):
     import warnings
     from functools import wraps
 
     def inner(f):
         @wraps(f)
         def wrapped(*args, **kwargs):
             with warnings.catch_warnings():
                 warnings.simplefilter("ignore", category=warning)
                 return f(*args, **kwargs)
         return wrapped
     return inner
diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index 3ce459e44..28380f6d4 100755
--- a/smoketest/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -1,350 +1,350 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import unittest
 import tempfile
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 from vyos.utils.process import cmd
 from vyos.utils.process import process_named_running
 
 DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf'
 DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
 DDCLIENT_PNAME = 'ddclient'
 
 base_path = ['service', 'dns', 'dynamic']
 name_path = base_path + ['name']
 server = 'ddns.vyos.io'
 hostname = 'test.ddns.vyos.io'
 zone = 'vyos.io'
 username = 'vyos_user'
 password = 'paSS_@4ord'
 ttl = '300'
 interface = 'eth0'
 
 class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
     def setUp(self):
         # Always start with a clean CLI instance
         self.cli_delete(base_path)
 
     def tearDown(self):
         # Check for running process
-        self.assertTrue(process_named_running(DDCLIENT_PNAME))
+        self.assertTrue(process_named_running(DDCLIENT_PNAME, timeout=5))
 
         # Delete DDNS configuration
         self.cli_delete(base_path)
         self.cli_commit()
 
         # Check for process not running anymore
         self.assertFalse(process_named_running(DDCLIENT_PNAME))
 
     # IPv4 standard DDNS service configuration
     def test_01_dyndns_service_standard(self):
         services = {'cloudflare': {'protocol': 'cloudflare'},
                     'freedns': {'protocol': 'freedns', 'username': username},
                     'zoneedit': {'protocol': 'zoneedit1', 'username': username}}
 
         for svc, details in services.items():
             self.cli_set(name_path + [svc, 'address', 'interface', interface])
             self.cli_set(name_path + [svc, 'host-name', hostname])
             self.cli_set(name_path + [svc, 'password', password])
             for opt, value in details.items():
                 self.cli_set(name_path + [svc, opt, value])
 
             # 'zone' option is supported by 'cloudfare' and 'zoneedit1', but not 'freedns'
             self.cli_set(name_path + [svc, 'zone', zone])
             if details['protocol'] in ['cloudflare', 'zoneedit1']:
                 pass
             else:
                 # exception is raised for unsupported ones
                 with self.assertRaises(ConfigSessionError):
                     self.cli_commit()
                 self.cli_delete(name_path + [svc, 'zone'])
 
             # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit'
             self.cli_set(name_path + [svc, 'ttl', ttl])
             if details['protocol'] == 'cloudflare':
                 pass
             else:
                 # exception is raised for unsupported ones
                 with self.assertRaises(ConfigSessionError):
                     self.cli_commit()
                 self.cli_delete(name_path + [svc, 'ttl'])
 
             # commit changes
             self.cli_commit()
 
             # Check the generating config parameters
             ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
             # default value 300 seconds
             self.assertIn(f'daemon=300', ddclient_conf)
             self.assertIn(f'usev4=ifv4', ddclient_conf)
             self.assertIn(f'ifv4={interface}', ddclient_conf)
             self.assertIn(f'password=\'{password}\'', ddclient_conf)
 
             for opt in details.keys():
                 if opt == 'username':
                     login = details[opt]
                     self.assertIn(f'login={login}', ddclient_conf)
                 else:
                     tmp = details[opt]
                     self.assertIn(f'{opt}={tmp}', ddclient_conf)
 
     # IPv6 only DDNS service configuration
     def test_02_dyndns_service_ipv6(self):
         interval = '60'
         svc_path = name_path + ['dynv6']
         proto = 'dyndns2'
         ip_version = 'ipv6'
         wait_time = '600'
         expiry_time_good = '3600'
         expiry_time_bad = '360'
 
         self.cli_set(base_path + ['interval', interval])
         self.cli_set(svc_path + ['address', 'interface', interface])
         self.cli_set(svc_path + ['ip-version', ip_version])
         self.cli_set(svc_path + ['protocol', proto])
         self.cli_set(svc_path + ['server', server])
         self.cli_set(svc_path + ['username', username])
         self.cli_set(svc_path + ['password', password])
         self.cli_set(svc_path + ['host-name', hostname])
         self.cli_set(svc_path + ['wait-time', wait_time])
 
         # expiry-time must be greater than wait-time, exception is raised otherwise
         with self.assertRaises(ConfigSessionError):
             self.cli_set(svc_path + ['expiry-time', expiry_time_bad])
             self.cli_commit()
         self.cli_set(svc_path + ['expiry-time', expiry_time_good])
 
         # commit changes
         self.cli_commit()
 
         # Check the generating config parameters
         ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
         self.assertIn(f'daemon={interval}', ddclient_conf)
         self.assertIn(f'usev6=ifv6', ddclient_conf)
         self.assertIn(f'ifv6={interface}', ddclient_conf)
         self.assertIn(f'protocol={proto}', ddclient_conf)
         self.assertIn(f'server={server}', ddclient_conf)
         self.assertIn(f'login={username}', ddclient_conf)
         self.assertIn(f'password=\'{password}\'', ddclient_conf)
         self.assertIn(f'min-interval={wait_time}', ddclient_conf)
         self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf)
 
     # IPv4+IPv6 dual DDNS service configuration
     def test_03_dyndns_service_dual_stack(self):
         services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
                     'freedns': {'protocol': 'freedns', 'username': username},
                     'google': {'protocol': 'googledomains', 'username': username}}
         ip_version = 'both'
 
         for name, details in services.items():
             self.cli_set(name_path + [name, 'address', 'interface', interface])
             self.cli_set(name_path + [name, 'host-name', hostname])
             self.cli_set(name_path + [name, 'password', password])
             for opt, value in details.items():
                 self.cli_set(name_path + [name, opt, value])
 
             # Dual stack is supported by 'cloudfare' and 'freedns' but not 'googledomains'
             # exception is raised for unsupported ones
             self.cli_set(name_path + [name, 'ip-version', ip_version])
             if details['protocol'] not in ['cloudflare', 'freedns']:
                 with self.assertRaises(ConfigSessionError):
                     self.cli_commit()
                 self.cli_delete(name_path + [name, 'ip-version'])
 
             # commit changes
             self.cli_commit()
 
             # Check the generating config parameters
             ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
             if details['protocol'] not in ['cloudflare', 'freedns']:
                 self.assertIn(f'usev4=ifv4', ddclient_conf)
                 self.assertIn(f'ifv4={interface}', ddclient_conf)
             else:
                 self.assertIn(f'usev4=ifv4', ddclient_conf)
                 self.assertIn(f'usev6=ifv6', ddclient_conf)
                 self.assertIn(f'ifv4={interface}', ddclient_conf)
                 self.assertIn(f'ifv6={interface}', ddclient_conf)
             self.assertIn(f'password=\'{password}\'', ddclient_conf)
 
             for opt in details.keys():
                 if opt == 'username':
                     login = details[opt]
                     self.assertIn(f'login={login}', ddclient_conf)
                 else:
                     tmp = details[opt]
                     self.assertIn(f'{opt}={tmp}', ddclient_conf)
 
     def test_04_dyndns_rfc2136(self):
         # Check if DDNS service can be configured and runs
         svc_path = name_path + ['vyos']
         proto = 'nsupdate'
 
         with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file:
             key_file.write(b'S3cretKey')
 
             self.cli_set(svc_path + ['address', 'interface', interface])
             self.cli_set(svc_path + ['protocol', proto])
             self.cli_set(svc_path + ['server', server])
             self.cli_set(svc_path + ['zone', zone])
             self.cli_set(svc_path + ['key', key_file.name])
             self.cli_set(svc_path + ['ttl', ttl])
             self.cli_set(svc_path + ['host-name', hostname])
 
             # commit changes
             self.cli_commit()
 
             # Check some generating config parameters
             ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
             self.assertIn(f'use=if', ddclient_conf)
             self.assertIn(f'if={interface}', ddclient_conf)
             self.assertIn(f'protocol={proto}', ddclient_conf)
             self.assertIn(f'server={server}', ddclient_conf)
             self.assertIn(f'zone={zone}', ddclient_conf)
             self.assertIn(f'password=\'{key_file.name}\'', ddclient_conf)
             self.assertIn(f'ttl={ttl}', ddclient_conf)
 
     def test_05_dyndns_hostname(self):
         # Check if DDNS service can be configured and runs
         svc_path = name_path + ['namecheap']
         proto = 'namecheap'
         hostnames = ['@', 'www', hostname, f'@.{hostname}']
 
         for name in hostnames:
             self.cli_set(svc_path + ['address', 'interface', interface])
             self.cli_set(svc_path + ['protocol', proto])
             self.cli_set(svc_path + ['server', server])
             self.cli_set(svc_path + ['username', username])
             self.cli_set(svc_path + ['password', password])
             self.cli_set(svc_path + ['host-name', name])
 
             # commit changes
             self.cli_commit()
 
             # Check the generating config parameters
             ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
             self.assertIn(f'protocol={proto}', ddclient_conf)
             self.assertIn(f'server={server}', ddclient_conf)
             self.assertIn(f'login={username}', ddclient_conf)
             self.assertIn(f'password=\'{password}\'', ddclient_conf)
             self.assertIn(f'{name}', ddclient_conf)
 
     def test_06_dyndns_web_options(self):
         # Check if DDNS service can be configured and runs
         svc_path = name_path + ['cloudflare']
         proto = 'cloudflare'
         web_url = 'https://ifconfig.me/ip'
         web_skip = 'Current IP Address:'
 
         self.cli_set(svc_path + ['protocol', proto])
         self.cli_set(svc_path + ['zone', zone])
         self.cli_set(svc_path + ['password', password])
         self.cli_set(svc_path + ['host-name', hostname])
 
         # not specifying either 'interface' or 'web' will raise an exception
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(svc_path + ['address', 'web'])
 
         # specifying both 'interface' and 'web' will raise an exception as well
         with self.assertRaises(ConfigSessionError):
             self.cli_set(svc_path + ['address', 'interface', interface])
             self.cli_commit()
         self.cli_delete(svc_path + ['address', 'interface'])
         self.cli_commit()
 
         # web option 'skip' is useless without the option 'url'
         with self.assertRaises(ConfigSessionError):
             self.cli_set(svc_path + ['address', 'web', 'skip', web_skip])
             self.cli_commit()
         self.cli_set(svc_path + ['address', 'web', 'url', web_url])
         self.cli_commit()
 
         # Check the generating config parameters
         ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
         self.assertIn(f'usev4=webv4', ddclient_conf)
         self.assertIn(f'webv4={web_url}', ddclient_conf)
         self.assertIn(f'webv4-skip=\'{web_skip}\'', ddclient_conf)
         self.assertIn(f'protocol={proto}', ddclient_conf)
         self.assertIn(f'zone={zone}', ddclient_conf)
         self.assertIn(f'password=\'{password}\'', ddclient_conf)
         self.assertIn(f'{hostname}', ddclient_conf)
 
     def test_07_dyndns_dynamic_interface(self):
         # Check if DDNS service can be configured and runs
         svc_path = name_path + ['namecheap']
         proto = 'namecheap'
         dyn_interface = 'pppoe587'
 
         self.cli_set(svc_path + ['address', 'interface', dyn_interface])
         self.cli_set(svc_path + ['protocol', proto])
         self.cli_set(svc_path + ['server', server])
         self.cli_set(svc_path + ['username', username])
         self.cli_set(svc_path + ['password', password])
         self.cli_set(svc_path + ['host-name', hostname])
 
         # Dynamic interface will raise a warning but still go through
         # XXX: We should have idiomatic class "ConfigSessionWarning" wrapping
         #      "Warning" similar to "ConfigSessionError".
         # with self.assertWarns(Warning):
         #     self.cli_commit()
         self.cli_commit()
 
         # Check the generating config parameters
         ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
         self.assertIn(f'ifv4={dyn_interface}', ddclient_conf)
         self.assertIn(f'protocol={proto}', ddclient_conf)
         self.assertIn(f'server={server}', ddclient_conf)
         self.assertIn(f'login={username}', ddclient_conf)
         self.assertIn(f'password=\'{password}\'', ddclient_conf)
         self.assertIn(f'{hostname}', ddclient_conf)
 
     def test_08_dyndns_vrf(self):
         # Table number randomized, but should be within range 100-65535
         vrf_table = '58710'
         vrf_name = f'vyos-test-{vrf_table}'
         svc_path = name_path + ['cloudflare']
         proto = 'cloudflare'
 
         self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table])
         self.cli_set(base_path + ['vrf', vrf_name])
 
         self.cli_set(svc_path + ['address', 'interface', interface])
         self.cli_set(svc_path + ['protocol', proto])
         self.cli_set(svc_path + ['host-name', hostname])
         self.cli_set(svc_path + ['zone', zone])
         self.cli_set(svc_path + ['password', password])
 
         # commit changes
         self.cli_commit()
 
         # Check for process in VRF
         systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}')
         self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}',
                       systemd_override)
 
         # Check for process in VRF
         proc = cmd(f'ip vrf pids {vrf_name}')
         self.assertIn(DDCLIENT_PNAME, proc)
 
         # Cleanup VRF
         self.cli_delete(['vrf', 'name', vrf_name])
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)