diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index ba97f37f6..fde58651a 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -1,290 +1,278 @@
 #!/bin/bash
 
 # Turn off Debian default for %sudo
 sed -i -e '/^%sudo/d' /etc/sudoers || true
 
 # Add minion user for salt-minion
 if ! grep -q '^minion' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg \
         --gecos "salt minion user" --shell /bin/vbash minion
     adduser --quiet minion frrvty
     adduser --quiet minion sudo
     adduser --quiet minion adm
     adduser --quiet minion dip
     adduser --quiet minion disk
     adduser --quiet minion users
     adduser --quiet minion frr
 fi
 
 # OpenVPN should get its own user
 if ! grep -q '^openvpn' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --group --shell /usr/sbin/nologin openvpn
 fi
 
 # node_exporter should get its own user
 if ! grep -q '^node_exporter' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --group --shell /bin/false node_exporter
 fi
 
 # We need to have a group for RADIUS service users to use it inside PAM rules
 if ! grep -q '^radius' /etc/group; then
     addgroup --firstgid 1000 --quiet radius
 fi
 
 # Remove TACACS user added by base package - we use our own UID range and group
 # assignments - see below
 if grep -q '^tacacs' /etc/passwd; then
     if [ $(id -u tacacs0) -ge 1000 ]; then
         level=0
         vyos_group=vyattaop
         while [ $level -lt 16 ]; do
             userdel tacacs${level} || true
             rm -rf /home/tacacs${level} || true
             level=$(( level+1 ))
         done 2>&1
     fi
 fi
 
 # Remove TACACS+ PAM default profile
 if [[ -e /usr/share/pam-configs/tacplus ]]; then
     rm /usr/share/pam-configs/tacplus
 fi
 
 # Add TACACS system users required for TACACS based system authentication
 if ! grep -q '^tacacs' /etc/passwd; then
     # Add the tacacs group and all 16 possible tacacs privilege-level users to
     # the password file, home directories, etc. The accounts are not enabled
     # for local login, since they are only used to provide uid/gid/homedir for
     # the mapped TACACS+ logins (and lookups against them). The tacacs15 user
     # is also added to the sudo group, and vyattacfg group rather than vyattaop
     # (used for tacacs0-14).
     level=0
     vyos_group=vyattaop
     while [ $level -lt 16 ]; do
         adduser --quiet --system --firstuid 900 --disabled-login --ingroup tacacs \
             --no-create-home --gecos "TACACS+ mapped user at privilege level ${level}" \
             --shell /bin/vbash tacacs${level}
         adduser --quiet tacacs${level} frrvty
         adduser --quiet tacacs${level} adm
         adduser --quiet tacacs${level} dip
         adduser --quiet tacacs${level} users
         if [ $level -lt 15 ]; then
             adduser --quiet tacacs${level} vyattaop
             adduser --quiet tacacs${level} operator
         else
             adduser --quiet tacacs${level} vyattacfg
             adduser --quiet tacacs${level} sudo
             adduser --quiet tacacs${level} disk
             adduser --quiet tacacs${level} frr
             adduser --quiet tacacs${level} _kea
         fi
         level=$(( level+1 ))
     done 2>&1 | grep -v "User tacacs${level} already exists"
 fi
 
 # Add RADIUS operator user for RADIUS authenticated users to map to
 if ! grep -q '^radius_user' /etc/passwd; then
     adduser --quiet --firstuid 1000 --disabled-login --ingroup radius \
         --no-create-home --gecos "RADIUS mapped user at privilege level operator" \
         --shell /sbin/radius_shell radius_user
     adduser --quiet radius_user frrvty
     adduser --quiet radius_user vyattaop
     adduser --quiet radius_user operator
     adduser --quiet radius_user adm
     adduser --quiet radius_user dip
     adduser --quiet radius_user users
 fi
 
 # Add RADIUS admin user for RADIUS authenticated users to map to
 if ! grep -q '^radius_priv_user' /etc/passwd; then
     adduser --quiet --firstuid 1000 --disabled-login --ingroup radius \
         --no-create-home --gecos "RADIUS mapped user at privilege level admin" \
         --shell /sbin/radius_shell radius_priv_user
     adduser --quiet radius_priv_user frrvty
     adduser --quiet radius_priv_user vyattacfg
     adduser --quiet radius_priv_user sudo
     adduser --quiet radius_priv_user adm
     adduser --quiet radius_priv_user dip
     adduser --quiet radius_priv_user disk
     adduser --quiet radius_priv_user users
     adduser --quiet radius_priv_user frr
     adduser --quiet radius_priv_user _kea
 fi
 
 # add hostsd group for vyos-hostsd
 if ! grep -q '^hostsd' /etc/group; then
     addgroup --quiet --system hostsd
 fi
 
 # Add _kea user for kea-dhcp{4,6}-server to vyattacfg
 # The user should exist via kea-common installed as transitive dependency
 if grep -q '^_kea' /etc/passwd; then
     adduser --quiet _kea vyattacfg
 fi
 
 # ensure the proxy user has a proper shell
 chsh -s /bin/sh proxy
 
 # Set file capabilities
 setcap cap_net_admin=pe /sbin/ethtool
 setcap cap_net_admin=pe /sbin/tc
 setcap cap_net_admin=pe /bin/ip
 setcap cap_net_admin=pe /sbin/xtables-legacy-multi
 setcap cap_net_admin=pe /sbin/xtables-nft-multi
 setcap cap_net_admin=pe /usr/sbin/conntrack
 setcap cap_net_admin=pe /usr/sbin/arp
 setcap cap_net_raw=pe /usr/bin/tcpdump
 setcap cap_net_admin,cap_sys_admin=pe /sbin/sysctl
 setcap cap_sys_module=pe /bin/kmod
 setcap cap_sys_time=pe /bin/date
 
 # create needed directories
 mkdir -p /var/log/user
 mkdir -p /var/core
 mkdir -p /opt/vyatta/etc/config/auth
 mkdir -p /opt/vyatta/etc/config/scripts
 mkdir -p /opt/vyatta/etc/config/user-data
 mkdir -p /opt/vyatta/etc/config/support
 chown -R root:vyattacfg /opt/vyatta/etc/config
 chmod -R 775 /opt/vyatta/etc/config
 mkdir -p /opt/vyatta/etc/logrotate
 mkdir -p /opt/vyatta/etc/netdevice.d
 
 touch /etc/environment
 
 if [ ! -f /etc/bash_completion ]; then
   echo "source /etc/bash_completion.d/10vyatta-op" > /etc/bash_completion
   echo "source /etc/bash_completion.d/20vyatta-cfg" >> /etc/bash_completion
 fi
 
 sed -i 's/^set /builtin set /' /etc/bash_completion
 
 # Fix up PAM configuration for login so that invalid users are prompted
 # for password
 sed -i 's/requisite[ \t][ \t]*pam_securetty.so/required pam_securetty.so/' $rootfsdir/etc/pam.d/login
 
 # Change default shell for new accounts
 sed -i -e ':^DSHELL:s:/bin/bash:/bin/vbash:' /etc/adduser.conf
 
 # Do not allow users to change full name field (controlled by vyos-1x)
 sed -i -e 's/^CHFN_RESTRICT/#&/' /etc/login.defs
 
 # Only allow root to use passwd command
 if ! grep -q 'pam_succeed_if.so' /etc/pam.d/passwd ; then
     sed -i -e '/^@include/i \
 password    requisite pam_succeed_if.so user = root
 ' /etc/pam.d/passwd
 fi
 
 # remove unnecessary ddclient script in /etc/ppp/ip-up.d/
 # this logs unnecessary messages trying to start ddclient
 rm -f /etc/ppp/ip-up.d/ddclient
 
 # create /opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
 PRECONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
 if [ ! -x $PRECONFIG_SCRIPT ]; then
     mkdir -p $(dirname $PRECONFIG_SCRIPT)
     touch $PRECONFIG_SCRIPT
     chmod 755 $PRECONFIG_SCRIPT
     cat <<EOF >>$PRECONFIG_SCRIPT
 #!/bin/sh
 # This script is executed at boot time before VyOS configuration is applied.
 # Any modifications required to work around unfixed bugs or use
 # services not available through the VyOS CLI system can be placed here.
 
 EOF
 fi
 
-# cracklib-runtime default database location
-CRACKLIB_DIR=/var/cache/cracklib
-CRACKLIB_DB=cracklib_dict
-
 # create /opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 POSTCONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 if [ ! -x $POSTCONFIG_SCRIPT ]; then
     mkdir -p $(dirname $POSTCONFIG_SCRIPT)
     touch $POSTCONFIG_SCRIPT
     chmod 755 $POSTCONFIG_SCRIPT
     cat <<EOF >>$POSTCONFIG_SCRIPT
 #!/bin/sh
 # This script is executed at boot time after VyOS configuration is fully applied.
 # Any modifications required to work around unfixed bugs
 # or use services not available through the VyOS CLI system can be placed here.
-#
-# T6353 - Just in case, check if cracklib was installed properly
-# If the database file is missing, re-install the runtime package
-#
-if [ ! -f "${CRACKLIB_DIR}/${CRACKLIB_DB}.pwd" ]; then
-    mkdir -p $CRACKLIB_DIR
-    /usr/sbin/create-cracklib-dict -o $CRACKLIB_DIR/$CRACKLIB_DB \
-        /usr/share/dict/cracklib-small
-fi
+
 EOF
 fi
 
 # symlink destination is deleted during ISO assembly - this generates some noise
 # when the system boots: systemd-sysv-generator[1881]: stat() failed on
 # /etc/init.d/README, ignoring: No such file or directory. Thus we simply drop
 # the file.
 if [ -L /etc/init.d/README ]; then
     rm -f /etc/init.d/README
 fi
 
 # Remove unwanted daemon files from /etc
 # conntackd
 # pmacct
 # fastnetmon
 # ntp
 DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
         /etc/default/pmacctd /etc/pmacct
         /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf
         /etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts
         /etc/powerdns /etc/default/pdns-recursor
         /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns"
 for tmp in $DELETE; do
     if [ -e ${tmp} ]; then
         rm -rf ${tmp}
     fi
 done
 
 # Remove logrotate items controlled via CLI and VyOS defaults
 sed -i '/^\/var\/log\/messages$/d' /etc/logrotate.d/rsyslog
 sed -i '/^\/var\/log\/auth.log$/d' /etc/logrotate.d/rsyslog
 
 # Fix FRR pam.d "vtysh_pam" vtysh_pam: Failed in account validation T5110
 if test -f /etc/pam.d/frr; then
     if grep -q 'pam_rootok.so' /etc/pam.d/frr; then
         sed -i -re 's/rootok/permit/' /etc/pam.d/frr
     fi
 fi
 
 # Enable Cloud-init pre-configuration service
 systemctl enable vyos-config-cloud-init.service
 
 # Enable Podman API
 systemctl enable podman.service
 
 # Generate API GraphQL schema
 /usr/libexec/vyos/services/api/graphql/generate/generate_schema.py
 
 # Update XML cache
 python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py
 
 # Generate hardlinks for systemd units for multi VRF support
 # as softlinks will fail in systemd:
 # symlink target name type "ssh.service" does not match source, rejecting.
 if [ ! -f /lib/systemd/system/ssh@.service ]; then
     ln /lib/systemd/system/ssh.service /lib/systemd/system/ssh@.service
 fi
 
 # T4287 - as we have a non-signed kernel use the upstream wireless reulatory database
 update-alternatives --set regulatory.db /lib/firmware/regulatory.db-upstream
 
 # Restart vyos-configd to apply changes in Python scripts/templates
 if systemctl is-active --quiet vyos-configd; then
     systemctl restart vyos-configd
 fi
 # Restart vyos-domain-resolver if running
 if systemctl is-active --quiet vyos-domain-resolver; then
     systemctl restart vyos-domain-resolver
 fi
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index a27d8a28a..5d0e3464a 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -1,115 +1,121 @@
 # authutils -- miscelanneous functions for handling passwords and publis keys
 #
 # Copyright (C) 2023-2024 VyOS maintainers and contributors
 #
 # This library is free software; you can redistribute it and/or modify it under the terms of
 # the GNU Lesser General Public License as published by the Free Software Foundation;
 # either version 2.1 of the License, or (at your option) any later version.
 #
 # This library 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 Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License along with this library;
 # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import cracklib
 import math
 import re
 import string
 
 from enum import StrEnum
 from decimal import Decimal
 from vyos.utils.process import cmd
 
 
-DEFAULT_PASSWORD = 'vyos'
-LOW_ENTROPY_MSG = 'should be at least 8 characters long;'
-WEAK_PASSWORD_MSG= 'The password complexity is too low - @MSG@'
-
+DEFAULT_PASSWORD: str = 'vyos'
+LOW_ENTROPY_MSG: str = 'should be at least 8 characters long;'
+WEAK_PASSWORD_MSG: str = 'The password complexity is too low - @MSG@'
+CRACKLIB_ERROR_MSG: str = 'A following error occurred: @MSG@\n' \
+    'Possibly the cracklib database is corrupted or is missing. ' \
+    'Try reinstalling the python3-cracklib package.'
 
 class EPasswdStrength(StrEnum):
     WEAK = 'Weak'
     DECENT = 'Decent'
     STRONG = 'Strong'
+    ERROR = 'Cracklib Error'
 
 
 def calculate_entropy(charset: str, passwd: str) -> float:
     """
     Calculate the entropy of a password based on the set of characters used
     Uses E = log2(R**L) formula, where
         - R is the range (length) of the character set
         - L is the length of password
     """
     return math.log(math.pow(len(charset), len(passwd)), 2)
 
 def evaluate_strength(passwd: str) -> dict[str, str]:
     """ Evaluates password strength and returns a check result dict """
     charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
         string.punctuation + string.digits)
 
     result = {
         'strength': '',
         'error': '',
     }
 
     try:
         cracklib.FascistCheck(passwd)
     except ValueError as e:
         # The password is vulnerable to dictionary attack no matter the entropy
         if 'is' in str(e):
             msg = str(e).replace('is', 'should not be')
         else:
             msg = f'should not be {e}'
         result.update(strength=EPasswdStrength.WEAK)
         result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg))
+    except Exception as e:
+        result.update(strength=EPasswdStrength.ERROR)
+        result.update(error=CRACKLIB_ERROR_MSG.replace('@MSG@', str(e)))
     else:
         # Now check the password's entropy
         # Cast to Decimal for more precise rounding
         entropy = Decimal.from_float(calculate_entropy(charset, passwd))
 
         match round(entropy):
             case e if e in range(0, 59):
                 result.update(strength=EPasswdStrength.WEAK)
                 result.update(
                     error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG)
                 )
             case e if e in range(60, 119):
                 result.update(strength=EPasswdStrength.DECENT)
             case e if e >= 120:
                 result.update(strength=EPasswdStrength.STRONG)
 
     return result
 
 def make_password_hash(password):
     """ Makes a password hash for /etc/shadow using mkpasswd """
 
     mkpassword = 'mkpasswd --method=sha-512 --stdin'
     return cmd(mkpassword, input=password, timeout=5)
 
 def split_ssh_public_key(key_string, defaultname=""):
     """ Splits an SSH public key into its components """
 
     key_string = key_string.strip()
     parts = re.split(r'\s+', key_string)
 
     if len(parts) == 3:
         key_type, key_data, key_name = parts[0], parts[1], parts[2]
     else:
         key_type, key_data, key_name = parts[0], parts[1], defaultname
 
     if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
         raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))
 
     return({"type": key_type, "data": key_data, "name": key_name})
 
 def get_current_user() -> str:
     import os
     current_user = 'nobody'
     # During CLI "owner" script execution we use SUDO_USER
     if 'SUDO_USER' in os.environ:
         current_user = os.environ['SUDO_USER']
     # During op-mode or config-mode interactive CLI we use USER
     elif 'USER' in os.environ:
         current_user = os.environ['USER']
     return current_user
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py
index 1e6061ecf..3fed6d273 100755
--- a/src/conf_mode/system_login.py
+++ b/src/conf_mode/system_login.py
@@ -1,439 +1,440 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020-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 warnings
 
 from passlib.hosts import linux_context
 from psutil import users
 from pwd import getpwall
 from pwd import getpwnam
 from pwd import getpwuid
 from sys import exit
 from time import sleep
 
 from vyos.base import Warning
 from vyos.config import Config
 from vyos.configverify import verify_vrf
 from vyos.template import render
 from vyos.template import is_ipv4
 from vyos.utils.auth import (
     DEFAULT_PASSWORD,
     EPasswdStrength,
     evaluate_strength,
     get_current_user
 )
 from vyos.utils.configfs import delete_cli_node
 from vyos.utils.configfs import add_cli_node
 from vyos.utils.dict import dict_search
 from vyos.utils.file import chown
 from vyos.utils.process import cmd
 from vyos.utils.process import call
 from vyos.utils.process import run
 from vyos.utils.process import DEVNULL
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 autologout_file = "/etc/profile.d/autologout.sh"
 limits_file = "/etc/security/limits.d/10-vyos.conf"
 radius_config_file = "/etc/pam_radius_auth.conf"
 tacacs_pam_config_file = "/etc/tacplus_servers"
 tacacs_nss_config_file = "/etc/tacplus_nss.conf"
 nss_config_file = "/etc/nsswitch.conf"
 
 # Minimum UID used when adding system users
 MIN_USER_UID: int = 1000
 # Maximim UID used when adding system users
 MAX_USER_UID: int = 59999
 # LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec
 MAX_RADIUS_TIMEOUT: int = 50
 # MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout)
 MAX_RADIUS_COUNT: int = 8
 # Maximum number of supported TACACS servers
 MAX_TACACS_COUNT: int = 8
 # Minimum USER id for TACACS users
 MIN_TACACS_UID = 900
 # List of local user accounts that must be preserved
 SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1',
                               'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6',
                               'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11',
                               'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15']
 
 def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID):
     """Return list of dynamically allocated users (see Debian Policy Manual)"""
     local_users = []
     for s_user in getpwall():
         if getpwnam(s_user.pw_name).pw_uid < min_uid:
             continue
         if getpwnam(s_user.pw_name).pw_uid > max_uid:
             continue
         if s_user.pw_name in SYSTEM_USER_SKIP_LIST:
             continue
         local_users.append(s_user.pw_name)
 
     return local_users
 
 def get_shadow_password(username):
     with open('/etc/shadow') as f:
         for user in f.readlines():
             items = user.split(":")
             if username == items[0]:
                 return items[1]
     return None
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['system', 'login']
     login = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  no_tag_node_value_mangle=True,
                                  get_first_key=True,
                                  with_recursive_defaults=True)
 
     # users no longer existing in the running configuration need to be deleted
     local_users = get_local_users()
     cli_users = []
     if 'user' in login:
         cli_users = list(login['user'])
 
     # prune TACACS global defaults if not set by user
     if login.from_defaults(['tacacs']):
         del login['tacacs']
     # same for RADIUS
     if login.from_defaults(['radius']):
         del login['radius']
 
     # create a list of all users, cli and users
     all_users = list(set(local_users + cli_users))
     # We will remove any normal users that dos not exist in the current
     # configuration. This can happen if user is added but configuration was not
     # saved and system is rebooted.
     rm_users = [tmp for tmp in all_users if tmp not in cli_users]
     if rm_users: login.update({'rm_users' : rm_users})
 
     # Build TACACS user mapping
     if 'tacacs' in login:
         login['exclude_users'] = get_local_users(min_uid=0,
                                                  max_uid=MIN_TACACS_UID) + cli_users
         login['tacacs_min_uid'] = MIN_TACACS_UID
 
     return login
 
 def verify(login):
     if 'rm_users' in login:
         # This check is required as the script is also executed from vyos-router
         # init script and there is no SUDO_USER environment variable available
         # during system boot.
         tmp = get_current_user()
         if tmp in login['rm_users']:
             raise ConfigError(f'Attempting to delete current user: {tmp}')
 
     if 'user' in login:
         system_users = getpwall()
         for user, user_config in login['user'].items():
             # Linux system users range up until UID 1000, we can not create a
             # VyOS CLI user which already exists as system user
             for s_user in system_users:
                 if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
                     raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
 
             # T6353: Check password for complexity using cracklib.
             # A user password should be sufficiently complex
             plaintext_password = dict_search(
                 path='authentication.plaintext_password',
                 dict_object=user_config
             ) or None
 
+            failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
             if plaintext_password is not None:
                 result = evaluate_strength(plaintext_password)
-                if result['strength'] == EPasswdStrength.WEAK:
+                if result['strength'] in failed_check_status:
                     Warning(result['error'])
 
             for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
                 if 'type' not in pubkey_options:
                     raise ConfigError(f'Missing type for public-key "{pubkey}"!')
                 if 'key' not in pubkey_options:
                     raise ConfigError(f'Missing key for public-key "{pubkey}"!')
 
     if {'radius', 'tacacs'} <= set(login):
         raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!')
 
     # At lease one RADIUS server must not be disabled
     if 'radius' in login:
         if 'server' not in login['radius']:
             raise ConfigError('No RADIUS server defined!')
         sum_timeout: int = 0
         radius_servers_count: int = 0
         fail = True
         for server, server_config in dict_search('radius.server', login).items():
             if 'key' not in server_config:
                 raise ConfigError(f'RADIUS server "{server}" requires key!')
             if 'disable' not in server_config:
                 sum_timeout += int(server_config['timeout'])
                 radius_servers_count += 1
                 fail = False
 
         if fail:
             raise ConfigError('All RADIUS servers are disabled')
 
         if radius_servers_count > MAX_RADIUS_COUNT:
             raise ConfigError(f'Number of RADIUS servers exceeded maximum of {MAX_RADIUS_COUNT}!')
 
         if sum_timeout > MAX_RADIUS_TIMEOUT:
             raise ConfigError('Sum of RADIUS servers timeouts '
                               'has to be less or eq 50 sec')
 
         verify_vrf(login['radius'])
 
         if 'source_address' in login['radius']:
             ipv4_count = 0
             ipv6_count = 0
             for address in login['radius']['source_address']:
                 if is_ipv4(address): ipv4_count += 1
                 else:                ipv6_count += 1
 
             if ipv4_count > 1:
                 raise ConfigError('Only one IPv4 source-address can be set!')
             if ipv6_count > 1:
                 raise ConfigError('Only one IPv6 source-address can be set!')
 
     if 'tacacs' in login:
         tacacs_servers_count: int = 0
         fail = True
         for server, server_config in dict_search('tacacs.server', login).items():
             if 'key' not in server_config:
                 raise ConfigError(f'TACACS server "{server}" requires key!')
             if 'disable' not in server_config:
                 tacacs_servers_count += 1
                 fail = False
 
         if fail:
             raise ConfigError('All RADIUS servers are disabled')
 
         if tacacs_servers_count > MAX_TACACS_COUNT:
             raise ConfigError(f'Number of TACACS servers exceeded maximum of {MAX_TACACS_COUNT}!')
 
         verify_vrf(login['tacacs'])
 
     if 'max_login_session' in login and 'timeout' not in login:
         raise ConfigError('"login timeout" must be configured!')
 
     return None
 
 
 def generate(login):
     # calculate users encrypted password
     if 'user' in login:
         for user, user_config in login['user'].items():
             tmp = dict_search('authentication.plaintext_password', user_config)
             if tmp:
                 encrypted_password = linux_context.hash(tmp)
                 login['user'][user]['authentication']['encrypted_password'] = encrypted_password
                 del login['user'][user]['authentication']['plaintext_password']
 
                 # Set default commands for re-adding user with encrypted password
                 del_user_plain = ['system', 'login', 'user', user, 'authentication', 'plaintext-password']
                 add_user_encrypt = ['system', 'login', 'user', user, 'authentication', 'encrypted-password']
 
                 delete_cli_node(del_user_plain)
                 add_cli_node(add_user_encrypt, value=encrypted_password)
 
             else:
                 try:
                     if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config):
                         # If the current encrypted bassword matches the encrypted password
                         # from the config - do not update it. This will remove the encrypted
                         # value from the system logs.
                         #
                         # The encrypted password will be set only once during the first boot
                         # after an image upgrade.
                         del login['user'][user]['authentication']['encrypted_password']
                 except:
                     pass
 
     ### RADIUS based user authentication
     if 'radius' in login:
         render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
                    permission=0o600, user='root', group='root')
     else:
         if os.path.isfile(radius_config_file):
             os.unlink(radius_config_file)
 
     ### TACACS+ based user authentication
     if 'tacacs' in login:
         render(tacacs_pam_config_file, 'login/tacplus_servers.j2', login,
                    permission=0o644, user='root', group='root')
         render(tacacs_nss_config_file, 'login/tacplus_nss.conf.j2', login,
                    permission=0o644, user='root', group='root')
     else:
         if os.path.isfile(tacacs_pam_config_file):
             os.unlink(tacacs_pam_config_file)
         if os.path.isfile(tacacs_nss_config_file):
             os.unlink(tacacs_nss_config_file)
 
     # NSS must always be present on the system
     render(nss_config_file, 'login/nsswitch.conf.j2', login,
                permission=0o644, user='root', group='root')
 
     # /etc/security/limits.d/10-vyos.conf
     if 'max_login_session' in login:
         render(limits_file, 'login/limits.j2', login,
                    permission=0o644, user='root', group='root')
     else:
         if os.path.isfile(limits_file):
             os.unlink(limits_file)
 
     if 'timeout' in login:
         render(autologout_file, 'login/autologout.j2', login,
                    permission=0o755, user='root', group='root')
     else:
         if os.path.isfile(autologout_file):
             os.unlink(autologout_file)
 
     return None
 
 
 def apply(login):
     enable_otp = False
     if 'user' in login:
         for user, user_config in login['user'].items():
             # make new user using vyatta shell and make home directory (-m),
             # default group of 100 (users)
             command = 'useradd --create-home --no-user-group '
             # check if user already exists:
             if user in get_local_users():
                 # update existing account
                 command = 'usermod'
 
             # all accounts use /bin/vbash
             command += ' --shell /bin/vbash'
             # we need to use '' quotes when passing formatted data to the shell
             # else it will not work as some data parts are lost in translation
             tmp = dict_search('authentication.encrypted_password', user_config)
             if tmp: command += f" --password '{tmp}'"
 
             tmp = dict_search('full_name', user_config)
             if tmp: command += f" --comment '{tmp}'"
 
             tmp = dict_search('home_directory', user_config)
             if tmp: command += f" --home '{tmp}'"
             else: command += f" --home '/home/{user}'"
 
             command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}'
             try:
                 cmd(command)
                 # we should not rely on the value stored in user_config['home_directory'], as a
                 # crazy user will choose username root or any other system user which will fail.
                 #
                 # XXX: Should we deny using root at all?
                 home_dir = getpwnam(user).pw_dir
                 # always re-render SSH keys with appropriate permissions
                 render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2',
                        user_config, permission=0o600,
                        formater=lambda _: _.replace("&quot;", '"'),
                        user=user, group='users')
             except Exception as e:
                 raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
 
             # T5875: ensure UID is properly set on home directory if user is re-added
             # the home directory will always exist, as it's created above by --create-home,
             # retrieve current owner of home directory and adjust on demand
             dir_owner = None
             try:
                 dir_owner = getpwuid(os.stat(home_dir).st_uid).pw_name
             except:
                 pass
 
             if dir_owner != user:
                     chown(home_dir, user=user, recursive=True)
 
             # Generate 2FA/MFA One-Time-Pad configuration
             if dict_search('authentication.otp.key', user_config):
                 enable_otp = True
                 render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2',
                        user_config, permission=0o400, user=user, group='users')
             else:
                 # delete configuration as it's not enabled for the user
                 if os.path.exists(f'{home_dir}/.google_authenticator'):
                     os.remove(f'{home_dir}/.google_authenticator')
 
             # Lock/Unlock local user account
             lock_unlock = '--unlock'
             if 'disable' in user_config:
                 lock_unlock = '--lock'
             cmd(f'usermod {lock_unlock} {user}')
 
     if 'rm_users' in login:
         for user in login['rm_users']:
             try:
                 # Disable user to prevent re-login
                 call(f'usermod -s /sbin/nologin {user}')
 
                 # Logout user if he is still logged in
                 if user in list(set([tmp[0] for tmp in users()])):
                     print(f'{user} is logged in, forcing logout!')
                     # re-run command until user is logged out
                     while run(f'pkill -HUP -u {user}'):
                         sleep(0.250)
 
                 # Remove user account but leave home directory in place. Re-run
                 # command until user is removed - userdel might return 8 as
                 # SSH sessions are not all yet properly cleaned away, thus we
                 # simply re-run the command until the account wen't away
                 while run(f'userdel {user}', stderr=DEVNULL):
                     sleep(0.250)
 
             except Exception as e:
                 raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
 
     # Enable/disable RADIUS in PAM configuration
     cmd('pam-auth-update --disable radius-mandatory radius-optional')
     if 'radius' in login:
         if login['radius'].get('security_mode', '') == 'mandatory':
             pam_profile = 'radius-mandatory'
         else:
             pam_profile = 'radius-optional'
         cmd(f'pam-auth-update --enable {pam_profile}')
 
     # Enable/disable TACACS+ in PAM configuration
     cmd('pam-auth-update --disable tacplus-mandatory tacplus-optional')
     if 'tacacs' in login:
         if login['tacacs'].get('security_mode', '') == 'mandatory':
             pam_profile = 'tacplus-mandatory'
         else:
             pam_profile = 'tacplus-optional'
         cmd(f'pam-auth-update --enable {pam_profile}')
 
     # Enable/disable Google authenticator
     cmd('pam-auth-update --disable mfa-google-authenticator')
     if enable_otp:
         cmd(f'pam-auth-update --enable mfa-google-authenticator')
 
     return None
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index c6e9c7f6f..82756daec 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,1111 +1,1112 @@
 #!/usr/bin/env python3
 #
 # Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This file is part of VyOS.
 #
 # VyOS is free software: you can redistribute it and/or modify it under the
 # terms of the GNU General Public License as published by the Free Software
 # Foundation, either version 3 of the License, or (at your option) any later
 # version.
 #
 # VyOS 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
 # VyOS. If not, see <https://www.gnu.org/licenses/>.
 
 from argparse import ArgumentParser, Namespace
 from pathlib import Path
 from shutil import copy, chown, rmtree, copytree
 from glob import glob
 from sys import exit
 from os import environ
 from os import readlink
 from os import getpid, getppid
 from typing import Union
 from urllib.parse import urlparse
 from passlib.hosts import linux_context
 from errno import ENOSPC
 
 from psutil import disk_partitions
 
 from vyos.base import Warning
 from vyos.configtree import ConfigTree
 from vyos.remote import download
 from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER
 from vyos.template import render
 from vyos.utils.auth import (
     DEFAULT_PASSWORD,
     EPasswdStrength,
     evaluate_strength
 )
 from vyos.utils.io import ask_input, ask_yes_no, select_entry
 from vyos.utils.file import chmod_2775
 from vyos.utils.process import cmd, run, rc_cmd
 from vyos.version import get_version_data
 
 # define text messages
 MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system image" instead.'
 MSG_ERR_LIVE: str = 'The system is in live-boot mode. Please use "install image" instead.'
 MSG_ERR_NO_DISK: str = 'No suitable disk was found. There must be at least one disk of 2GB or greater size.'
 MSG_ERR_IMPROPER_IMAGE: str = 'Missing sha256sum.txt.\nEither this image is corrupted, or of era 1.2.x (md5sum) and would downgrade image tools;\ndisallowed in either case.'
 MSG_ERR_INCOMPATIBLE_IMAGE: str = 'Image compatibility check failed, aborting installation.'
 MSG_ERR_ARCHITECTURE_MISMATCH: str = 'The current architecture is "{0}", the new image is for "{1}". Upgrading to a different image architecture will break your system.'
 MSG_ERR_FLAVOR_MISMATCH: str = 'The current image flavor is "{0}", the new image is "{1}". Upgrading to a non-matching flavor can have unpredictable consequences.'
 MSG_ERR_MISSING_ARCHITECTURE: str = 'The new image version data does not specify architecture, cannot check compatibility (is it a legacy release image?)'
 MSG_ERR_MISSING_FLAVOR: str = 'The new image version data does not specify flavor, cannot check compatibility (is it a legacy release image?)'
 MSG_ERR_CORRUPT_CURRENT_IMAGE: str = 'Version data in the current image is malformed: missing flavor and/or architecture fields. Upgrade compatibility cannot be checked.'
 MSG_INFO_INSTALL_WELCOME: str = 'Welcome to VyOS installation!\nThis command will install VyOS to your permanent storage.'
 MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation'
 MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.'
 MSG_INFO_INSTALL_DISKS_LIST: str = 'The following disks were found:'
 MSG_INFO_INSTALL_DISK_SELECT: str = 'Which one should be used for installation?'
 MSG_INFO_INSTALL_RAID_CONFIGURE: str = 'Would you like to configure RAID-1 mirroring?'
 MSG_INFO_INSTALL_RAID_FOUND_DISKS: str = 'Would you like to configure RAID-1 mirroring on them?'
 MSG_INFO_INSTALL_RAID_CHOOSE_DISKS: str = 'Would you like to choose two disks for RAID-1 mirroring?'
 MSG_INFO_INSTALL_DISK_CONFIRM: str = 'Installation will delete all data on the drive. Continue?'
 MSG_INFO_INSTALL_RAID_CONFIRM: str = 'Installation will delete all data on both drives. Continue?'
 MSG_INFO_INSTALL_PARTITONING: str = 'Creating partition table...'
 MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like to copy it to the new image?'
 MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boot:'
 MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?'
 MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?'
 MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?'
 MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'
 MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'
 MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?'
 MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?'
 MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial)?'
 MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
 MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
 MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?'
 MSG_INPUT_CHOOSE_COPY_ENC_DATA: str = 'From which image would you like to copy the encrypted config?'
 MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?'
 MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
 MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
 MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again'
 MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\
 'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9'
 
 MSG_WARN_CHANGE_PASSWORD: str = 'Default password used. Consider changing ' \
     'it on next login.'
 MSG_WARN_PASSWORD_CONFIRM: str = 'The entered values did not match. Try again'
 'Installing a different image flavor may cause functionality degradation or break your system.\n' \
 'Do you want to continue with installation?'
 CONST_MIN_DISK_SIZE: int = 2147483648  # 2 GB
 CONST_MIN_ROOT_SIZE: int = 1610612736  # 1.5 GB
 # a reserved space: 2MB for header, 1 MB for BIOS partition, 256 MB for EFI
 CONST_RESERVED_SPACE: int = (2 + 1 + 256) * 1024**2
 
 # define directories and paths
 DIR_INSTALLATION: str = '/mnt/installation'
 DIR_ROOTFS_SRC: str = f'{DIR_INSTALLATION}/root_src'
 DIR_ROOTFS_DST: str = f'{DIR_INSTALLATION}/root_dst'
 DIR_ISO_MOUNT: str = f'{DIR_INSTALLATION}/iso_src'
 DIR_DST_ROOT: str = f'{DIR_INSTALLATION}/disk_dst'
 DIR_KERNEL_SRC: str = '/boot/'
 FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs'
 ISO_DOWNLOAD_PATH: str = ''
 
 external_download_script = '/usr/libexec/vyos/simple-download.py'
 external_latest_image_url_script = '/usr/libexec/vyos/latest-image-url.py'
 
 # default boot variables
 DEFAULT_BOOT_VARS: dict[str, str] = {
     'timeout': '5',
     'console_type': 'tty',
     'console_num': '0',
     'console_speed': '115200',
     'bootmode': 'normal'
 }
 
 
 def bytes_to_gb(size: int) -> float:
     """Convert Bytes to GBytes, rounded to 1 decimal number
 
     Args:
         size (int): input size in bytes
 
     Returns:
         float: size in GB
     """
     return round(size / 1024**3, 1)
 
 
 def gb_to_bytes(size: float) -> int:
     """Convert GBytes to Bytes
 
     Args:
         size (float): input size in GBytes
 
     Returns:
         int: size in bytes
     """
     return int(size * 1024**3)
 
 
 def find_disks() -> dict[str, int]:
     """Find a target disk for installation
 
     Returns:
         dict[str, int]: a list of available disks by name and size
     """
     # check for available disks
     print('Probing disks')
     disks_available: dict[str, int] = disk.disks_size()
     for disk_name, disk_size in disks_available.copy().items():
         if disk_size < CONST_MIN_DISK_SIZE:
             del disks_available[disk_name]
     if not disks_available:
         print(MSG_ERR_NO_DISK)
         exit(MSG_INFO_INSTALL_EXIT)
 
     num_disks: int = len(disks_available)
     print(f'{num_disks} disk(s) found')
 
     return disks_available
 
 
 def ask_root_size(available_space: int) -> int:
     """Define a size of root partition
 
     Args:
         available_space (int): available space in bytes for a root partition
 
     Returns:
         int: defined size
     """
     if ask_yes_no(MSG_INPUT_ROOT_SIZE_ALL, default=True):
         return available_space
 
     while True:
         root_size_gb: str = ask_input(MSG_INPUT_ROOT_SIZE_SET)
         root_size_kbytes: int = (gb_to_bytes(float(root_size_gb))) // 1024
 
         if root_size_kbytes > available_space:
             print(MSG_WARN_ROOT_SIZE_TOOBIG)
             continue
         if root_size_kbytes < CONST_MIN_ROOT_SIZE / 1024:
             print(MSG_WARN_ROOT_SIZE_TOOSMALL)
             continue
 
         return root_size_kbytes
 
 def create_partitions(target_disk: str, target_size: int,
                       prompt: bool = True) -> None:
     """Create partitions on a target disk
 
     Args:
         target_disk (str): a target disk
         target_size (int): size of disk in bytes
     """
     # define target rootfs size in KB (smallest unit acceptable by sgdisk)
     available_size: int = (target_size - CONST_RESERVED_SPACE) // 1024
     if prompt:
         rootfs_size: int = ask_root_size(available_size)
     else:
         rootfs_size: int = available_size
 
     print(MSG_INFO_INSTALL_PARTITONING)
     raid.clear()
     disk.disk_cleanup(target_disk)
     disk_details: disk.DiskDetails = disk.parttable_create(target_disk,
                                                            rootfs_size)
 
     return disk_details
 
 
 def search_format_selection(image: tuple[str, str]) -> str:
     """Format a string for selection of image
 
     Args:
         image (tuple[str, str]): a tuple of image name and drive
 
     Returns:
         str: formatted string
     """
     return f'{image[0]} on {image[1]}'
 
 
 def search_previous_installation(disks: list[str]) -> None:
     """Search disks for previous installation config and SSH keys
 
     Args:
         disks (list[str]): a list of available disks
     """
     mnt_config = '/mnt/config'
     mnt_encrypted_config = '/mnt/encrypted_config'
     mnt_ssh = '/mnt/ssh'
     mnt_tmp = '/mnt/tmp'
     rmtree(Path(mnt_config), ignore_errors=True)
     rmtree(Path(mnt_ssh), ignore_errors=True)
     Path(mnt_tmp).mkdir(exist_ok=True)
     Path(mnt_encrypted_config).unlink(missing_ok=True)
 
     print('Searching for data from previous installations')
     image_data = []
     encrypted_configs = []
     for disk_name in disks:
         for partition in disk.partition_list(disk_name):
             if disk.partition_mount(partition, mnt_tmp):
                 if Path(mnt_tmp + '/boot').exists():
                     for path in Path(mnt_tmp + '/boot').iterdir():
                         if path.joinpath('rw/config/.vyatta_config').exists():
                             image_data.append((path.name, partition))
                 if Path(mnt_tmp + '/luks').exists():
                     for path in Path(mnt_tmp + '/luks').iterdir():
                         encrypted_configs.append((path.name, partition))
 
                 disk.partition_umount(partition)
 
     image_name = None
     image_drive = None
     encrypted = False
 
     if len(image_data) > 0:
         if len(image_data) == 1:
             print('Found data from previous installation:')
             print(f'\t{" on ".join(image_data[0])}')
             if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
                 image_name, image_drive = image_data[0]
 
         elif len(image_data) > 1:
             print('Found data from previous installations')
             if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
                 image_name, image_drive = select_entry(image_data,
                                                        'Available versions:',
                                                        MSG_INPUT_CHOOSE_COPY_DATA,
                                                        search_format_selection)
     elif len(encrypted_configs) > 0:
         if len(encrypted_configs) == 1:
             print('Found encrypted config from previous installation:')
             print(f'\t{" on ".join(encrypted_configs[0])}')
             if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
                 image_name, image_drive = encrypted_configs[0]
                 encrypted = True
 
         elif len(encrypted_configs) > 1:
             print('Found encrypted configs from previous installations')
             if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
                 image_name, image_drive = select_entry(encrypted_configs,
                                           'Available versions:',
                                           MSG_INPUT_CHOOSE_COPY_ENC_DATA,
                                           search_format_selection)
                 encrypted = True
 
     else:
         print('No previous installation found')
         return
 
     if not image_name:
         return
 
     disk.partition_mount(image_drive, mnt_tmp)
 
     if not encrypted:
         copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
     else:
         copy(f'{mnt_tmp}/luks/{image_name}', mnt_encrypted_config)
 
     Path(mnt_ssh).mkdir()
     host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*')
     for host_key in host_keys:
         copy(host_key, mnt_ssh)
 
     disk.partition_umount(image_drive)
 
 def copy_preserve_owner(src: str, dst: str, *, follow_symlinks=True):
     if not Path(src).is_file():
         return
     if Path(dst).is_dir():
         dst = Path(dst).joinpath(Path(src).name)
     st = Path(src).stat()
     copy(src, dst, follow_symlinks=follow_symlinks)
     chown(dst, user=st.st_uid)
 
 
 def copy_previous_installation_data(target_dir: str) -> None:
     if Path('/mnt/config').exists():
         copytree('/mnt/config', f'{target_dir}/opt/vyatta/etc/config',
                  dirs_exist_ok=True)
     if Path('/mnt/ssh').exists():
         copytree('/mnt/ssh', f'{target_dir}/etc/ssh',
                  dirs_exist_ok=True)
 
 
 def copy_previous_encrypted_config(target_dir: str, image_name: str) -> None:
     if Path('/mnt/encrypted_config').exists():
         Path(target_dir).mkdir(exist_ok=True)
         copy('/mnt/encrypted_config', Path(target_dir).joinpath(image_name))
 
 
 def ask_single_disk(disks_available: dict[str, int]) -> str:
     """Ask user to select a disk for installation
 
     Args:
         disks_available (dict[str, int]): a list of available disks
     """
     print(MSG_INFO_INSTALL_DISKS_LIST)
     default_disk: str = list(disks_available)[0]
     for disk_name, disk_size in disks_available.items():
         disk_size_human: str = bytes_to_gb(disk_size)
         print(f'Drive: {disk_name} ({disk_size_human} GB)')
     disk_selected: str = ask_input(MSG_INFO_INSTALL_DISK_SELECT,
                                    default=default_disk,
                                    valid_responses=list(disks_available))
 
     # create partitions
     if not ask_yes_no(MSG_INFO_INSTALL_DISK_CONFIRM):
         print(MSG_INFO_INSTALL_EXIT)
         exit()
 
     search_previous_installation(list(disks_available))
 
     disk_details: disk.DiskDetails = create_partitions(disk_selected,
                                                        disks_available[disk_selected])
 
     disk.filesystem_create(disk_details.partition['efi'], 'efi')
     disk.filesystem_create(disk_details.partition['root'], 'ext4')
 
     return disk_details
 
 
 def check_raid_install(disks_available: dict[str, int]) -> Union[str, None]:
     """Ask user to select disks for RAID installation
 
     Args:
         disks_available (dict[str, int]): a list of available disks
     """
     if len(disks_available) < 2:
         return None
 
     if not ask_yes_no(MSG_INFO_INSTALL_RAID_CONFIGURE, default=True):
         return None
 
     def format_selection(disk_name: str) -> str:
         return f'{disk_name}\t({bytes_to_gb(disks_available[disk_name])} GB)'
 
     disk0, disk1 = list(disks_available)[0], list(disks_available)[1]
     disks_selected: dict[str, int] = { disk0: disks_available[disk0],
                                        disk1: disks_available[disk1] }
 
     target_size: int = min(disks_selected[disk0], disks_selected[disk1])
 
     print(MSG_INFO_INSTALL_DISKS_LIST)
     for disk_name, disk_size in disks_selected.items():
         disk_size_human: str = bytes_to_gb(disk_size)
         print(f'\t{disk_name} ({disk_size_human} GB)')
     if not ask_yes_no(MSG_INFO_INSTALL_RAID_FOUND_DISKS, default=True):
         if not ask_yes_no(MSG_INFO_INSTALL_RAID_CHOOSE_DISKS, default=True):
             return None
         else:
             disks_selected = {}
             disk0 = select_entry(list(disks_available), 'Disks available:',
                                  'Select first disk:', format_selection)
 
             disks_selected[disk0] = disks_available[disk0]
             del disks_available[disk0]
             disk1 = select_entry(list(disks_available), 'Remaining disks:',
                                  'Select second disk:', format_selection)
             disks_selected[disk1] = disks_available[disk1]
 
             target_size: int = min(disks_selected[disk0],
                                    disks_selected[disk1])
 
     # create partitions
     if not ask_yes_no(MSG_INFO_INSTALL_RAID_CONFIRM):
         print(MSG_INFO_INSTALL_EXIT)
         exit()
 
     search_previous_installation(list(disks_available))
 
     disks: list[disk.DiskDetails] = []
     for disk_selected in list(disks_selected):
         print(f'Creating partitions on {disk_selected}')
         disk_details = create_partitions(disk_selected, target_size,
                                          prompt=False)
         disk.filesystem_create(disk_details.partition['efi'], 'efi')
 
         disks.append(disk_details)
 
     print('Creating RAID array')
     members = [disk.partition['root'] for disk in disks]
     raid_details: raid.RaidDetails = raid.raid_create(members)
     # raid init stuff
     print('Updating initramfs')
     raid.update_initramfs()
     # end init
     print('Creating filesystem on RAID array')
     disk.filesystem_create(raid_details.name, 'ext4')
 
     return raid_details
 
 
 def prepare_tmp_disr() -> None:
     """Create temporary directories for installation
     """
     print('Creating temporary directories')
     for dir in [DIR_ROOTFS_SRC, DIR_ROOTFS_DST, DIR_DST_ROOT]:
         dirpath = Path(dir)
         dirpath.mkdir(mode=0o755, parents=True)
 
 
 def setup_grub(root_dir: str) -> None:
     """Install GRUB configurations
 
     Args:
         root_dir (str): a path to the root of target filesystem
     """
     print('Installing GRUB configuration files')
     grub_cfg_main = f'{root_dir}/{grub.GRUB_DIR_MAIN}/grub.cfg'
     grub_cfg_vars = f'{root_dir}/{grub.CFG_VYOS_VARS}'
     grub_cfg_modules = f'{root_dir}/{grub.CFG_VYOS_MODULES}'
     grub_cfg_menu = f'{root_dir}/{grub.CFG_VYOS_MENU}'
     grub_cfg_options = f'{root_dir}/{grub.CFG_VYOS_OPTIONS}'
 
     # create new files
     render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {})
     grub.common_write(root_dir)
     grub.vars_write(grub_cfg_vars, DEFAULT_BOOT_VARS)
     grub.modules_write(grub_cfg_modules, [])
     grub.write_cfg_ver(1, root_dir)
     render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {})
     render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {})
 
 
 def configure_authentication(config_file: str, password: str) -> None:
     """Write encrypted password to config file
 
     Args:
         config_file (str): path of target config file
         password (str): plaintext password
 
     N.B. this can not be deferred by simply setting the plaintext password
     and relying on the config mode script to process at boot, as the config
     will not automatically be saved in that case, thus leaving the
     plaintext exposed
     """
     encrypted_password = linux_context.hash(password)
 
     with open(config_file) as f:
         config_string = f.read()
 
     config = ConfigTree(config_string)
     config.set([
         'system', 'login', 'user', 'vyos', 'authentication',
         'encrypted-password'
     ],
                value=encrypted_password,
                replace=True)
     config.set_tag(['system', 'login', 'user'])
 
     with open(config_file, 'w') as f:
         f.write(config.to_string())
 
 def validate_signature(file_path: str, sign_type: str) -> None:
     """Validate a file by signature and delete a signature file
 
     Args:
         file_path (str): a path to file
         sign_type (str): a signature type
     """
     print('Validating signature')
     signature_valid: bool = False
     # validate with minisig
     if sign_type == 'minisig':
         pub_key_list = glob('/usr/share/vyos/keys/*.minisign.pub')
         for pubkey in pub_key_list:
             if run(f'minisign -V -q -p {pubkey} -m {file_path} -x {file_path}.minisig'
                   ) == 0:
                 signature_valid = True
                 break
         Path(f'{file_path}.minisig').unlink()
     # validate with GPG
     if sign_type == 'asc':
         if run(f'gpg --verify ${file_path}.asc ${file_path}') == 0:
             signature_valid = True
         Path(f'{file_path}.asc').unlink()
 
     # warn or pass
     if not signature_valid:
         if not ask_yes_no(MSG_WARN_ISO_SIGN_INVALID, default=False):
             exit(MSG_INFO_INSTALL_EXIT)
     else:
         print('Signature is valid')
 
 def download_file(local_file: str, remote_path: str, vrf: str,
                   username: str, password: str,
                   progressbar: bool = False, check_space: bool = False):
     environ['REMOTE_USERNAME'] = username
     environ['REMOTE_PASSWORD'] = password
     if vrf is None:
         download(local_file, remote_path, progressbar=progressbar,
                  check_space=check_space, raise_error=True)
     else:
         remote_auth = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password}'
         vrf_cmd = f'ip vrf exec {vrf} {external_download_script} \
                     --local-file {local_file} --remote-path {remote_path}'
         cmd(vrf_cmd, auth=remote_auth)
 
 def image_fetch(image_path: str, vrf: str = None,
                 username: str = '', password: str = '',
                 no_prompt: bool = False) -> Path:
     """Fetch an ISO image
 
     Args:
         image_path (str): a path, remote or local
 
     Returns:
         Path: a path to a local file
     """
     import os.path
     from uuid import uuid4
 
     global ISO_DOWNLOAD_PATH
 
     # Latest version gets url from configured "system update-check url"
     if image_path == 'latest':
         command = external_latest_image_url_script
         if vrf:
             command = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \
                         ip vrf exec {vrf} ' + command
         code, output = rc_cmd(command)
         if code:
             print(output)
             exit(MSG_INFO_INSTALL_EXIT)
         image_path = output if output else image_path
 
     try:
         # check a type of path
         if urlparse(image_path).scheme:
             # download an image
             ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4()))
             download_file(ISO_DOWNLOAD_PATH, image_path, vrf,
                           username, password,
                           progressbar=True, check_space=True)
 
             # download a signature
             sign_file = (False, '')
             for sign_type in ['minisig', 'asc']:
                 try:
                     download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}',
                                   f'{image_path}.{sign_type}', vrf,
                                   username, password)
                     sign_file = (True, sign_type)
                     break
                 except Exception:
                     print(f'{sign_type} signature is not available')
             # validate a signature if it is available
             if sign_file[0]:
                 validate_signature(ISO_DOWNLOAD_PATH, sign_file[1])
             else:
                 if (not no_prompt and
                     not ask_yes_no(MSG_WARN_ISO_SIGN_UNAVAL, default=False)):
                     cleanup()
                     exit(MSG_INFO_INSTALL_EXIT)
 
             return Path(ISO_DOWNLOAD_PATH)
         else:
             local_path: Path = Path(image_path)
             if local_path.is_file():
                 return local_path
             else:
                 raise FileNotFoundError
     except Exception as e:
         print(f'The image cannot be fetched from: {image_path} {e}')
         exit(1)
 
 
 def migrate_config() -> bool:
     """Check for active config and ask user for migration
 
     Returns:
         bool: user's decision
     """
     active_config_path: Path = Path('/opt/vyatta/etc/config/config.boot')
     if active_config_path.exists():
         if ask_yes_no(MSG_INPUT_CONFIG_FOUND, default=True):
             return True
     return False
 
 
 def copy_ssh_host_keys() -> bool:
     """Ask user to copy SSH host keys
 
     Returns:
         bool: user's decision
     """
     if ask_yes_no('Would you like to copy SSH host keys?', default=True):
         return True
     return False
 
 
 def console_hint() -> str:
     pid = getppid() if 'SUDO_USER' in environ else getpid()
     try:
         path = readlink(f'/proc/{pid}/fd/1')
     except OSError:
         path = '/dev/tty'
 
     name = Path(path).name
     if name == 'ttyS0':
         return 'S'
     else:
         return 'K'
 
 
 def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:
     """Clean up after installation
 
     Args:
         mounts (list[str], optional): List of mounts to unmount.
         Defaults to [].
         remove_items (list[str], optional): List of files or directories
         to remove. Defaults to [].
     """
     print('Cleaning up')
     # clean up installation directory by default
     mounts_all = disk_partitions(all=True)
     for mounted_device in mounts_all:
         if mounted_device.mountpoint.startswith(DIR_INSTALLATION) and not (
                 mounted_device.device in mounts or
                 mounted_device.mountpoint in mounts):
             mounts.append(mounted_device.mountpoint)
     # add installation dir to cleanup list
     if DIR_INSTALLATION not in remove_items:
         remove_items.append(DIR_INSTALLATION)
     # also delete an ISO file
     if Path(ISO_DOWNLOAD_PATH).exists(
     ) and ISO_DOWNLOAD_PATH not in remove_items:
         remove_items.append(ISO_DOWNLOAD_PATH)
 
     if mounts:
         print('Unmounting target filesystems')
         for mountpoint in mounts:
             disk.partition_umount(mountpoint)
         for mountpoint in mounts:
             disk.wait_for_umount(mountpoint)
     if remove_items:
         print('Removing temporary files')
         for remove_item in remove_items:
             if Path(remove_item).exists():
                 if Path(remove_item).is_file():
                     Path(remove_item).unlink()
                 if Path(remove_item).is_dir():
                     rmtree(remove_item, ignore_errors=True)
 
 
 def cleanup_raid(details: raid.RaidDetails) -> None:
     efiparts = []
     for raid_disk in details.disks:
         efiparts.append(raid_disk.partition['efi'])
     cleanup([details.name, *efiparts],
             ['/mnt/installation'])
 
 
 def is_raid_install(install_object: Union[disk.DiskDetails, raid.RaidDetails]) -> bool:
     """Check if installation target is a RAID array
 
     Args:
         install_object (Union[disk.DiskDetails, raid.RaidDetails]): a target disk
 
     Returns:
         bool: True if it is a RAID array
     """
     if isinstance(install_object, raid.RaidDetails):
         return True
     return False
 
 
 def validate_compatibility(iso_path: str, force: bool = False) -> None:
     """Check architecture and flavor compatibility with the running image
 
     Args:
         iso_path (str): a path to the mounted ISO image
     """
     current_data = get_version_data()
     current_flavor = current_data.get('flavor')
     current_architecture = current_data.get('architecture') or cmd('dpkg --print-architecture')
 
     new_data = get_version_data(f'{iso_path}/version.json')
     new_flavor = new_data.get('flavor')
     new_architecture = new_data.get('architecture')
 
     if not current_flavor or not current_architecture:
         # This may only happen if someone modified the version file.
         # Unlikely but not impossible.
         print(MSG_ERR_CORRUPT_CURRENT_IMAGE)
         cleanup()
         exit(MSG_INFO_INSTALL_EXIT)
 
     success = True
 
     if current_architecture != new_architecture:
         success = False
         if not new_architecture:
             print(MSG_ERR_MISSING_ARCHITECTURE)
         else:
             print(MSG_ERR_ARCHITECTURE_MISMATCH.format(current_architecture, new_architecture))
 
     if current_flavor != new_flavor:
         if not force:
             success = False
         if not new_flavor:
             print(MSG_ERR_MISSING_FLAVOR)
         else:
             print(MSG_ERR_FLAVOR_MISMATCH.format(current_flavor, new_flavor))
 
     if not success:
         print(MSG_ERR_INCOMPATIBLE_IMAGE)
         cleanup()
         exit(MSG_INFO_INSTALL_EXIT)
 
 def install_image() -> None:
     """Install an image to a disk
     """
     if not image.is_live_boot():
         exit(MSG_ERR_NOT_LIVE)
 
     print(MSG_INFO_INSTALL_WELCOME)
     if not ask_yes_no('Would you like to continue?'):
         print(MSG_INFO_INSTALL_EXIT)
         exit()
 
     # configure image name
     running_image_name: str = image.get_running_image()
     while True:
         image_name: str = ask_input(MSG_INPUT_IMAGE_NAME,
                                     running_image_name)
         if image.validate_name(image_name):
             break
         print(MSG_WARN_IMAGE_NAME_WRONG)
 
+    failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
     # ask for password
     while True:
         user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True,
                                        non_empty=True)
 
         if user_password == DEFAULT_PASSWORD:
             Warning(MSG_WARN_CHANGE_PASSWORD)
         else:
             result = evaluate_strength(user_password)
-            if result['strength'] == EPasswdStrength.WEAK:
+            if result['strength'] in failed_check_status:
                 Warning(result['error'])
 
         confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True,
                                  non_empty=True)
 
         if user_password == confirm:
             break
 
         print(MSG_WARN_PASSWORD_CONFIRM)
 
     # ask for default console
     console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE,
                                   default=console_hint(),
                                   valid_responses=['K', 'S'])
     console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'}
 
     config_boot_list = ['/opt/vyatta/etc/config/config.boot',
                         '/opt/vyatta/etc/config.boot.default']
     default_config = config_boot_list[0]
 
     disks: dict[str, int] = find_disks()
 
     install_target: Union[disk.DiskDetails, raid.RaidDetails, None] = None
     try:
         install_target = check_raid_install(disks)
         if install_target is None:
             install_target = ask_single_disk(disks)
 
         # if previous install was selected in search_previous_installation,
         # directory /mnt/config was prepared for copy below; if not, prompt:
         if not Path('/mnt/config').exists():
             default_config: str = select_entry(config_boot_list,
                                                MSG_INPUT_CONFIG_CHOICE,
                                                MSG_INPUT_CONFIG_CHOOSE,
                                                default_entry=1) # select_entry indexes from 1
 
         # create directories for installation media
         prepare_tmp_disr()
 
         # mount target filesystem and create required dirs inside
         print('Mounting new partitions')
         if is_raid_install(install_target):
             disk.partition_mount(install_target.name, DIR_DST_ROOT)
             Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True)
         else:
             disk.partition_mount(install_target.partition['root'], DIR_DST_ROOT)
             Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True)
             disk.partition_mount(install_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')
 
         # a config dir. It is the deepest one, so the comand will
         # create all the rest in a single step
         print('Creating a configuration file')
         target_config_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw/opt/vyatta/etc/config/'
         Path(target_config_dir).mkdir(parents=True)
         chown(target_config_dir, group='vyattacfg')
         chmod_2775(target_config_dir)
         # copy config
         copy(default_config, f'{target_config_dir}/config.boot')
         configure_authentication(f'{target_config_dir}/config.boot',
                                  user_password)
         Path(f'{target_config_dir}/.vyatta_config').touch()
 
         # create a persistence.conf
         Path(f'{DIR_DST_ROOT}/persistence.conf').write_text('/ union\n')
 
         # copy system image and kernel files
         print('Copying system image files')
         for file in Path(DIR_KERNEL_SRC).iterdir():
             if file.is_file():
                 copy(file, f'{DIR_DST_ROOT}/boot/{image_name}/')
         copy(FILE_ROOTFS_SRC,
              f'{DIR_DST_ROOT}/boot/{image_name}/{image_name}.squashfs')
 
         # copy saved config data and SSH keys
         # owner restored on copy of config data by chmod_2775, above
         copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw')
 
         # copy saved encrypted config volume
         copy_previous_encrypted_config(f'{DIR_DST_ROOT}/luks', image_name)
 
         if is_raid_install(install_target):
             write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
             raid.update_default(write_dir)
 
         setup_grub(DIR_DST_ROOT)
         # add information about version
         grub.create_structure()
         grub.version_add(image_name, DIR_DST_ROOT)
         grub.set_default(image_name, DIR_DST_ROOT)
         grub.set_console_type(console_dict[console_type], DIR_DST_ROOT)
 
         if is_raid_install(install_target):
             # add RAID specific modules
             grub.modules_write(f'{DIR_DST_ROOT}/{grub.CFG_VYOS_MODULES}',
                                ['part_msdos', 'part_gpt', 'diskfilter',
                                 'ext2','mdraid1x'])
         # install GRUB
         if is_raid_install(install_target):
             print('Installing GRUB to the drives')
             l = install_target.disks
             for disk_target in l:
                 disk.partition_mount(disk_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')
                 grub.install(disk_target.name, f'{DIR_DST_ROOT}/boot/',
                              f'{DIR_DST_ROOT}/boot/efi',
                              id=f'VyOS (RAID disk {l.index(disk_target) + 1})')
                 disk.partition_umount(disk_target.partition['efi'])
         else:
             print('Installing GRUB to the drive')
             grub.install(install_target.name, f'{DIR_DST_ROOT}/boot/',
                          f'{DIR_DST_ROOT}/boot/efi')
 
         # sort inodes (to make GRUB read config files in alphabetical order)
         grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS}')
         grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS_VERS}')
 
         # umount filesystems and remove temporary files
         if is_raid_install(install_target):
             cleanup([install_target.name],
                     ['/mnt/installation'])
         else:
             cleanup([install_target.partition['efi'],
                      install_target.partition['root']],
                     ['/mnt/installation'])
 
         # we are done
         print(MSG_INFO_INSTALL_SUCCESS)
         exit()
 
     except Exception as err:
         print(f'Unable to install VyOS: {err}')
         # unmount filesystems and clenup
         try:
             if install_target is not None:
                 if is_raid_install(install_target):
                     cleanup_raid(install_target)
                 else:
                     cleanup([install_target.partition['efi'],
                              install_target.partition['root']],
                             ['/mnt/installation'])
         except Exception as err:
             print(f'Cleanup failed: {err}')
 
         exit(1)
 
 
 @compat.grub_cfg_update
 def add_image(image_path: str, vrf: str = None, username: str = '',
               password: str = '', no_prompt: bool = False, force: bool = False) -> None:
     """Add a new image
 
     Args:
         image_path (str): a path to an ISO image
     """
     if image.is_live_boot():
         exit(MSG_ERR_LIVE)
 
     # fetch an image
     iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt)
     try:
         # mount an ISO
         Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True)
         disk.partition_mount(iso_path, DIR_ISO_MOUNT, 'iso9660')
 
         print('Validating image compatibility')
         validate_compatibility(DIR_ISO_MOUNT, force=force)
 
         # check sums
         print('Validating image checksums')
         if not Path(DIR_ISO_MOUNT).joinpath('sha256sum.txt').exists():
             cleanup()
             exit(MSG_ERR_IMPROPER_IMAGE)
         if run(f'cd {DIR_ISO_MOUNT} && sha256sum --status -c sha256sum.txt'):
             cleanup()
             exit('Image checksum verification failed.')
 
         # mount rootfs (to get a system version)
         Path(DIR_ROOTFS_SRC).mkdir(mode=0o755, parents=True)
         disk.partition_mount(f'{DIR_ISO_MOUNT}/live/filesystem.squashfs',
                              DIR_ROOTFS_SRC, 'squashfs')
 
         cfg_ver: str = image.get_image_tools_version(DIR_ROOTFS_SRC)
         version_name: str = image.get_image_version(DIR_ROOTFS_SRC)
 
         disk.partition_umount(f'{DIR_ISO_MOUNT}/live/filesystem.squashfs')
 
         if cfg_ver < SYSTEM_CFG_VER:
             raise compat.DowngradingImageTools(
                 f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed')
 
         if not no_prompt:
             while True:
                 image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
                 if image.validate_name(image_name):
                     break
                 print(MSG_WARN_IMAGE_NAME_WRONG)
             set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True)
         else:
             image_name: str = version_name
             set_as_default: bool = True
 
         # find target directory
         root_dir: str = disk.find_persistence()
 
         # a config dir. It is the deepest one, so the comand will
         # create all the rest in a single step
         target_config_dir: str = f'{root_dir}/boot/{image_name}/rw/opt/vyatta/etc/config/'
         # copy config
         if no_prompt or migrate_config():
             print('Copying configuration directory')
             # copytree preserves perms but not ownership:
             Path(target_config_dir).mkdir(parents=True)
             chown(target_config_dir, group='vyattacfg')
             chmod_2775(target_config_dir)
             copytree('/opt/vyatta/etc/config/', target_config_dir,
                      copy_function=copy_preserve_owner, dirs_exist_ok=True)
         else:
             Path(target_config_dir).mkdir(parents=True)
             chown(target_config_dir, group='vyattacfg')
             chmod_2775(target_config_dir)
             Path(f'{target_config_dir}/.vyatta_config').touch()
 
         target_ssh_dir: str = f'{root_dir}/boot/{image_name}/rw/etc/ssh/'
         if no_prompt or copy_ssh_host_keys():
             print('Copying SSH host keys')
             Path(target_ssh_dir).mkdir(parents=True)
             host_keys: list[str] = glob('/etc/ssh/ssh_host*')
             for host_key in host_keys:
                 copy(host_key, target_ssh_dir)
 
         # copy system image and kernel files
         print('Copying system image files')
         for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir():
             if file.is_file() and (file.match('initrd*') or
                                    file.match('vmlinuz*')):
                 copy(file, f'{root_dir}/boot/{image_name}/')
         copy(f'{DIR_ISO_MOUNT}/live/filesystem.squashfs',
              f'{root_dir}/boot/{image_name}/{image_name}.squashfs')
 
         # unmount an ISO and cleanup
         cleanup([str(iso_path)])
 
         # add information about version
         grub.version_add(image_name, root_dir)
         if set_as_default:
             grub.set_default(image_name, root_dir)
 
     except OSError as e:
         # if no space error, remove image dir and cleanup
         if e.errno == ENOSPC:
             cleanup(mounts=[str(iso_path)],
                     remove_items=[f'{root_dir}/boot/{image_name}'])
         else:
             # unmount an ISO and cleanup
             cleanup([str(iso_path)])
         exit(f'Error: {e}')
 
     except Exception as err:
         # unmount an ISO and cleanup
         cleanup([str(iso_path)])
         exit(f'Error: {err}')
 
 
 def parse_arguments() -> Namespace:
     """Parse arguments
 
     Returns:
         Namespace: a namespace with parsed arguments
     """
     parser: ArgumentParser = ArgumentParser(
         description='Install new system images')
     parser.add_argument('--action',
                         choices=['install', 'add'],
                         required=True,
                         help='action to perform with an image')
     parser.add_argument('--vrf',
                         help='vrf name for image download')
     parser.add_argument('--no-prompt', action='store_true',
                         help='perform action non-interactively')
     parser.add_argument('--username', default='',
                         help='username for image download')
     parser.add_argument('--password', default='',
                         help='password for image download')
     parser.add_argument('--image-path',
         help='a path (HTTP or local file) to an image that needs to be installed'
     )
     parser.add_argument('--force', action='store_true',
         help='Ignore flavor compatibility requirements.'
     )
     # parser.add_argument('--image_new_name', help='a new name for image')
     args: Namespace = parser.parse_args()
     # Validate arguments
     if args.action == 'add' and not args.image_path:
         exit('A path to image is required for add action')
 
     return args
 
 
 if __name__ == '__main__':
     try:
         args: Namespace = parse_arguments()
         if args.action == 'install':
             install_image()
         if args.action == 'add':
             add_image(args.image_path, args.vrf,
                       args.username, args.password,
                       args.no_prompt, args.force)
 
         exit()
 
     except KeyboardInterrupt:
         print('Stopped by Ctrl+C')
         cleanup()
         exit()
 
     except Exception as err:
         exit(f'{err}')