diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl
index 089cdae3b..ab7b14d6b 100644
--- a/data/templates/frr/daemons.frr.tmpl
+++ b/data/templates/frr/daemons.frr.tmpl
@@ -1,50 +1,54 @@
 zebra=yes
 bgpd=yes
 ospfd=yes
 ospf6d=yes
 ripd=yes
 ripngd=yes
 isisd=yes
 pimd=no
 ldpd=yes
 nhrpd=no
 eigrpd=no
 babeld=no
 sharpd=no
 pbrd=no
 bfdd=yes
 staticd=yes
 
 vtysh_enable=yes
 zebra_options="  -s 90000000 --daemon -A 127.0.0.1
 {%- if irdp is defined %} -M irdp{% endif -%}
 {%- if snmp is defined and snmp.zebra is defined %} -M snmp{% endif -%}
 "
 bgpd_options="   --daemon -A 127.0.0.1
 {%- if bmp is defined %} -M bmp{% endif -%}
 {%- if snmp is defined and snmp.bgpd is defined %} -M snmp{% endif -%}
 "
 ospfd_options="  --daemon -A 127.0.0.1
 {%- if snmp is defined and snmp.ospfd is defined %} -M snmp{% endif -%}
 "
 ospf6d_options=" --daemon -A ::1
 {%- if snmp is defined and snmp.ospf6d is defined %} -M snmp{% endif -%}
 "
 ripd_options="   --daemon -A 127.0.0.1
 {%- if snmp is defined and snmp.ripd is defined %} -M snmp{% endif -%}
 "
 ripngd_options=" --daemon -A ::1"
-isisd_options="  --daemon -A 127.0.0.1"
+isisd_options="  --daemon -A 127.0.0.1
+{%- if snmp is defined and snmp.isisd is defined %} -M snmp{% endif -%}
+"
 pimd_options="  --daemon -A 127.0.0.1"
-ldpd_options="  --daemon -A 127.0.0.1"
+ldpd_options="  --daemon -A 127.0.0.1
+{%- if snmp is defined and snmp.ldpd is defined %} -M snmp{% endif -%}
+"
 nhrpd_options="  --daemon -A 127.0.0.1"
 eigrpd_options="  --daemon -A 127.0.0.1"
 babeld_options="  --daemon -A 127.0.0.1"
 sharpd_options="  --daemon -A 127.0.0.1"
 pbrd_options="  --daemon -A 127.0.0.1"
 staticd_options="  --daemon -A 127.0.0.1"
 bfdd_options="  --daemon -A 127.0.0.1"
 
 watchfrr_enable=no
 valgrind_enable=no
 
diff --git a/interface-definitions/system-frr.xml.in b/interface-definitions/system-frr.xml.in
index e8b447f58..9fe23ed75 100644
--- a/interface-definitions/system-frr.xml.in
+++ b/interface-definitions/system-frr.xml.in
@@ -1,66 +1,77 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interfaceDefinition>
   <node name="system">
     <children>
       <node name="frr" owner="${vyos_conf_scripts_dir}/system_frr.py">
         <properties>
           <help>Configure FRR parameters</help>
           <!-- Before components that use FRR -->
           <priority>150</priority>
         </properties>
         <children>
           <leafNode name="bmp">
             <properties>
-              <help>>Enable BGP Monitoring Protocol support</help>
+              <help>Enable BGP Monitoring Protocol support</help>
               <valueless/>
             </properties>
           </leafNode>
           <leafNode name="irdp">
             <properties>
-              <help>>Enable ICMP Router Discovery Protocol support</help>
+              <help>Enable ICMP Router Discovery Protocol support</help>
               <valueless/>
             </properties>
           </leafNode>
           <node name="snmp">
             <properties>
               <help>Enable SNMP integration for next daemons</help>
             </properties>
             <children>
               <leafNode name="bgpd">
                 <properties>
-                  <help>>BGP</help>
+                  <help>BGP</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <leafNode name="isisd">
+                <properties>
+                  <help>IS-IS</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <leafNode name="ldpd">
+                <properties>
+                  <help>LDP</help>
                   <valueless/>
                 </properties>
               </leafNode>
               <leafNode name="ospf6d">
                 <properties>
-                  <help>>OSPFv3</help>
+                  <help>OSPFv3</help>
                   <valueless/>
                 </properties>
               </leafNode>
               <leafNode name="ospfd">
                 <properties>
-                  <help>>OSPFv2</help>
+                  <help>OSPFv2</help>
                   <valueless/>
                 </properties>
               </leafNode>
               <leafNode name="ripd">
                 <properties>
-                  <help>>RIP</help>
+                  <help>RIP</help>
                   <valueless/>
                 </properties>
               </leafNode>
               <leafNode name="zebra">
                 <properties>
-                  <help>>Zebra (IP routing manager)</help>
+                  <help>Zebra (IP routing manager)</help>
                   <valueless/>
                 </properties>
               </leafNode>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
-
diff --git a/python/vyos/util.py b/python/vyos/util.py
index ce5dc51f5..d8e83ab8d 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1,967 +1,963 @@
 # Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # 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, see <http://www.gnu.org/licenses/>.
 
 import os
 import re
 import sys
 
 #
 # NOTE: Do not import full classes here, move your import to the function
 # where it is used so it is as local as possible to the execution
 #
 
 from subprocess import Popen
 from subprocess import PIPE
 from subprocess import STDOUT
 from subprocess import DEVNULL
 
 def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
           stdout=PIPE, stderr=PIPE, decode='utf-8'):
     """
     popen is a wrapper helper aound subprocess.Popen
     with it default setting it will return a tuple (out, err)
     out: the output of the program run
     err: the error code returned by the program
 
     it can be affected by the following flags:
     shell:   do not try to auto-detect if a shell is required
              for example if a pipe (|) or redirection (>, >>) is used
     input:   data to sent to the child process via STDIN
              the data should be bytes but string will be converted
     timeout: time after which the command will be considered to have failed
     env:     mapping that defines the environment variables for the new process
     stdout:  define how the output of the program should be handled
               - PIPE (default), sends stdout to the output
               - DEVNULL, discard the output
     stderr:  define how the output of the program should be handled
               - None (default), send/merge the data to/with stderr
               - PIPE, popen will append it to output
               - STDOUT, send the data to be merged with stdout
               - DEVNULL, discard the output
     decode:  specify the expected text encoding (utf-8, ascii, ...)
              the default is explicitely utf-8 which is python's own default
 
     usage:
     get both stdout and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
     discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
     """
 
     # airbag must be left as an import in the function as otherwise we have a
     # a circual import dependency
     from vyos import debug
     from vyos import airbag
 
     # log if the flag is set, otherwise log if command is set
     if not debug.enabled(flag):
         flag = 'command'
 
     cmd_msg = f"cmd '{command}'"
     debug.message(cmd_msg, flag)
 
     use_shell = shell
     stdin = None
     if shell is None:
         use_shell = False
         if ' ' in command:
             use_shell = True
         if env:
             use_shell = True
 
     if input:
         stdin = PIPE
         input = input.encode() if type(input) is str else input
 
     p = Popen(command, stdin=stdin, stdout=stdout, stderr=stderr,
               env=env, shell=use_shell)
 
     pipe = p.communicate(input, timeout)
 
     pipe_out = b''
     if stdout == PIPE:
         pipe_out = pipe[0]
 
     pipe_err = b''
     if stderr == PIPE:
         pipe_err = pipe[1]
 
     str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
     str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
 
     out_msg = f"returned (out):\n{str_out}"
     if str_out:
         debug.message(out_msg, flag)
 
     if str_err:
         err_msg = f"returned (err):\n{str_err}"
         # this message will also be send to syslog via airbag
         debug.message(err_msg, flag, destination=sys.stderr)
 
         # should something go wrong, report this too via airbag
         airbag.noteworthy(cmd_msg)
         airbag.noteworthy(out_msg)
         airbag.noteworthy(err_msg)
 
     return str_out, p.returncode
 
 
 def run(command, flag='', shell=None, input=None, timeout=None, env=None,
         stdout=DEVNULL, stderr=PIPE, decode='utf-8'):
     """
     A wrapper around popen, which discard the stdout and
     will return the error code of a command
     """
     _, code = popen(
         command, flag,
         stdout=stdout, stderr=stderr,
         input=input, timeout=timeout,
         env=env, shell=shell,
         decode=decode,
     )
     return code
 
 
 def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
         stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='',
         expect=[0]):
     """
     A wrapper around popen, which returns the stdout and
     will raise the error code of a command
 
     raising: specify which call should be used when raising
              the class should only require a string as parameter
              (default is OSError) with the error code
     expect:  a list of error codes to consider as normal
     """
     decoded, code = popen(
         command, flag,
         stdout=stdout, stderr=stderr,
         input=input, timeout=timeout,
         env=env, shell=shell,
         decode=decode,
     )
     if code not in expect:
         feedback = message + '\n' if message else ''
         feedback += f'failed to run command: {command}\n'
         feedback += f'returned: {decoded}\n'
         feedback += f'exit code: {code}'
         if raising is None:
             # error code can be recovered with .errno
             raise OSError(code, feedback)
         else:
             raise raising(feedback)
     return decoded
 
 
 def call(command, flag='', shell=None, input=None, timeout=None, env=None,
          stdout=PIPE, stderr=PIPE, decode='utf-8'):
     """
     A wrapper around popen, which print the stdout and
     will return the error code of a command
     """
     out, code = popen(
         command, flag,
         stdout=stdout, stderr=stderr,
         input=input, timeout=timeout,
         env=env, shell=shell,
         decode=decode,
     )
     if out:
         print(out)
     return code
 
 
-def read_file(fname, defaultonfailure=None, strip_end=True):
+def read_file(fname, defaultonfailure=None):
     """
-    read the content of a file, optionally stripping any end characters (space, newlines)
+    read the content of a file, stripping any end characters (space, newlines)
     should defaultonfailure be not None, it is returned on failure to read
     """
     try:
         """ Read a file to string """
         with open(fname, 'r') as f:
-            data = f.read()
-
-        if strip_end:
-            return data.strip()
-        else:
-            return data
+            data = f.read().strip()
+        return data
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None):
     """
     Write content of data to given fname, should defaultonfailure be not None,
     it is returned on failure to read.
 
     If directory of file is not present, it is auto-created.
     """
     dirname = os.path.dirname(fname)
     if not os.path.isdir(dirname):
         os.makedirs(dirname, mode=0o755, exist_ok=False)
         chown(dirname, user, group)
 
     try:
         """ Write a file to string """
         bytes = 0
         with open(fname, 'w') as f:
             bytes = f.write(data)
         chown(fname, user, group)
         chmod(fname, mode)
         return bytes
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 def read_json(fname, defaultonfailure=None):
     """
     read and json decode the content of a file
     should defaultonfailure be not None, it is returned on failure to read
     """
     import json
     try:
         with open(fname, 'r') as f:
             data = json.load(f)
         return data
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 
 def chown(path, user, group):
     """ change file/directory owner """
     from pwd import getpwnam
     from grp import getgrnam
 
     if user is None or group is None:
         return False
 
     # path may also be an open file descriptor
     if not isinstance(path, int) and not os.path.exists(path):
         return False
 
     uid = getpwnam(user).pw_uid
     gid = getgrnam(group).gr_gid
     os.chown(path, uid, gid)
     return True
 
 
 def chmod(path, bitmask):
     # path may also be an open file descriptor
     if not isinstance(path, int) and not os.path.exists(path):
         return
     if bitmask is None:
         return
     os.chmod(path, bitmask)
 
 
 def chmod_600(path):
     """ make file only read/writable by owner """
     from stat import S_IRUSR, S_IWUSR
 
     bitmask = S_IRUSR | S_IWUSR
     chmod(path, bitmask)
 
 
 def chmod_750(path):
     """ make file/directory only executable to user and group """
     from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
 
     bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
     chmod(path, bitmask)
 
 
 def chmod_755(path):
     """ make file executable by all """
     from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
 
     bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
               S_IROTH | S_IXOTH
     chmod(path, bitmask)
 
 
 def makedir(path, user=None, group=None):
     if os.path.exists(path):
         return
     os.makedirs(path, mode=0o755)
     chown(path, user, group)
 
 def colon_separated_to_dict(data_string, uniquekeys=False):
     """ Converts a string containing newline-separated entries
         of colon-separated key-value pairs into a dict.
 
         Such files are common in Linux /proc filesystem
 
     Args:
         data_string (str): data string
         uniquekeys (bool): whether to insist that keys are unique or not
 
     Returns: dict
 
     Raises:
         ValueError: if uniquekeys=True and the data string has
             duplicate keys.
 
     Note:
         If uniquekeys=True, then dict entries are always strings,
         otherwise they are always lists of strings.
     """
     import re
     key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
 
     data_raw = re.split('\n', data_string)
 
     data = {}
 
     for l in data_raw:
         l = l.strip()
         if l:
             match = re.match(key_value_re, l)
             if match:
                 key = match.groups()[0].strip()
                 value = match.groups()[1].strip()
             if key in data.keys():
                 if uniquekeys:
                     raise ValueError("Data string has duplicate keys: {0}".format(key))
                 else:
                     data[key].append(value)
             else:
                 if uniquekeys:
                     data[key] = value
                 else:
                     data[key] = [value]
         else:
             pass
 
     return data
 
 def _mangle_dict_keys(data, regex, replacement, abs_path=[], no_tag_node_value_mangle=False, mod=0):
     """ Mangles dict keys according to a regex and replacement character.
     Some libraries like Jinja2 do not like certain characters in dict keys.
     This function can be used for replacing all offending characters
     with something acceptable.
 
     Args:
         data (dict): Original dict to mangle
 
     Returns: dict
     """
     from vyos.xml import is_tag
 
     new_dict = {}
 
     for key in data.keys():
         save_mod = mod
         save_path = abs_path[:]
 
         abs_path.append(key)
 
         if not is_tag(abs_path):
             new_key = re.sub(regex, replacement, key)
         else:
             if mod%2:
                 new_key = key
             else:
                 new_key = re.sub(regex, replacement, key)
             if no_tag_node_value_mangle:
                 mod += 1
 
         value = data[key]
 
         if isinstance(value, dict):
             new_dict[new_key] = _mangle_dict_keys(value, regex, replacement, abs_path=abs_path, mod=mod, no_tag_node_value_mangle=no_tag_node_value_mangle)
         else:
             new_dict[new_key] = value
 
         mod = save_mod
         abs_path = save_path[:]
 
     return new_dict
 
 def mangle_dict_keys(data, regex, replacement, abs_path=[], no_tag_node_value_mangle=False):
     return _mangle_dict_keys(data, regex, replacement, abs_path=abs_path, no_tag_node_value_mangle=no_tag_node_value_mangle, mod=0)
 
 def _get_sub_dict(d, lpath):
     k = lpath[0]
     if k not in d.keys():
         return {}
     c = {k: d[k]}
     lpath = lpath[1:]
     if not lpath:
         return c
     elif not isinstance(c[k], dict):
         return {}
     return _get_sub_dict(c[k], lpath)
 
 def get_sub_dict(source, lpath, get_first_key=False):
     """ Returns the sub-dict of a nested dict, defined by path of keys.
 
     Args:
         source (dict): Source dict to extract from
         lpath (list[str]): sequence of keys
 
     Returns: source, if lpath is empty, else
              {key : source[..]..[key]} for key the last element of lpath, if exists
              {} otherwise
     """
     if not isinstance(source, dict):
         raise TypeError("source must be of type dict")
     if not isinstance(lpath, list):
         raise TypeError("path must be of type list")
     if not lpath:
         return source
 
     ret =  _get_sub_dict(source, lpath)
 
     if get_first_key and lpath and ret:
         tmp = next(iter(ret.values()))
         if not isinstance(tmp, dict):
             raise TypeError("Data under node is not of type dict")
         ret = tmp
 
     return ret
 
 def process_running(pid_file):
     """ Checks if a process with PID in pid_file is running """
     from psutil import pid_exists
     if not os.path.isfile(pid_file):
         return False
     with open(pid_file, 'r') as f:
         pid = f.read().strip()
     return pid_exists(int(pid))
 
 def process_named_running(name):
     """ Checks if process with given name is running and returns its PID.
     If Process is not running, return None
     """
     from psutil import process_iter
     for p in process_iter():
         if name in p.name():
             return p.pid
     return None
 
 def seconds_to_human(s, separator=""):
     """ Converts number of seconds passed to a human-readable
     interval such as 1w4d18h35m59s
     """
     s = int(s)
 
     week = 60 * 60 * 24 * 7
     day = 60 * 60 * 24
     hour = 60 * 60
 
     remainder = 0
     result = ""
 
     weeks = s // week
     if weeks > 0:
         result = "{0}w".format(weeks)
         s = s % week
 
     days = s // day
     if days > 0:
         result = "{0}{1}{2}d".format(result, separator, days)
         s = s % day
 
     hours = s // hour
     if hours > 0:
         result = "{0}{1}{2}h".format(result, separator, hours)
         s = s % hour
 
     minutes = s // 60
     if minutes > 0:
         result = "{0}{1}{2}m".format(result, separator, minutes)
         s = s % 60
 
     seconds = s
     if seconds > 0:
         result = "{0}{1}{2}s".format(result, separator, seconds)
 
     return result
 
 def bytes_to_human(bytes, initial_exponent=0):
     """ Converts a value in bytes to a human-readable size string like 640 KB
 
     The initial_exponent parameter is the exponent of 2,
     e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes.
     """
 
     from math import log2
 
     bytes = bytes * (2**initial_exponent)
 
     # log2 is a float, while range checking requires an int
     exponent = int(log2(bytes))
 
     if exponent < 10:
         value = bytes
         suffix = "B"
     elif exponent in range(10, 20):
         value = bytes / 1024
         suffix = "KB"
     elif exponent in range(20, 30):
         value = bytes / 1024**2
         suffix = "MB"
     elif exponent in range(30, 40):
         value = bytes / 1024**3
         suffix = "GB"
     else:
         value = bytes / 1024**4
         suffix = "TB"
     # Add a new case when the first machine with petabyte RAM
     # hits the market.
 
     size_string = "{0:.2f} {1}".format(value, suffix)
     return size_string
 
 def get_cfg_group_id():
     from grp import getgrnam
     from vyos.defaults import cfg_group
 
     group_data = getgrnam(cfg_group)
     return group_data.gr_gid
 
 
 def file_is_persistent(path):
     import re
     location = r'^(/config|/opt/vyatta/etc/config)'
     absolute = os.path.abspath(os.path.dirname(path))
     return re.match(location,absolute)
 
 def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sleep_interval=0.1):
     """ Waits for an inotify event to occur """
     if not os.path.dirname(file_path):
         raise ValueError(
           "File path {} does not have a directory part (required for inotify watching)".format(file_path))
     if not os.path.basename(file_path):
         raise ValueError(
           "File path {} does not have a file part, do not know what to watch for".format(file_path))
 
     from inotify.adapters import Inotify
     from time import time
     from time import sleep
 
     time_start = time()
 
     i = Inotify()
     i.add_watch(os.path.dirname(file_path))
 
     if pre_hook:
         pre_hook()
 
     for event in i.event_gen(yield_nones=True):
         if (timeout is not None) and ((time() - time_start) > timeout):
             # If the function didn't return until this point,
             # the file failed to have been written to and closed within the timeout
             raise OSError("Waiting for file {} to be written has failed".format(file_path))
 
         # Most such events don't take much time, so it's better to check right away
         # and sleep later.
         if event is not None:
             (_, type_names, path, filename) = event
             if filename == os.path.basename(file_path):
                 if event_type in type_names:
                     return
         sleep(sleep_interval)
 
 def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_interval=0.1):
     """ Waits for a process to close a file after opening it in write mode. """
     wait_for_inotify(file_path,
       event_type='IN_CLOSE_WRITE', pre_hook=pre_hook, timeout=timeout, sleep_interval=sleep_interval)
 
 def commit_in_progress():
     """ Not to be used in normal op mode scripts! """
 
     # The CStore backend locks the config by opening a file
     # The file is not removed after commit, so just checking
     # if it exists is insufficient, we need to know if it's open by anyone
 
     # There are two ways to check if any other process keeps a file open.
     # The first one is to try opening it and see if the OS objects.
     # That's faster but prone to race conditions and can be intrusive.
     # The other one is to actually check if any process keeps it open.
     # It's non-intrusive but needs root permissions, else you can't check
     # processes of other users.
     #
     # Since this will be used in scripts that modify the config outside of the CLI
     # framework, those knowingly have root permissions.
     # For everything else, we add a safeguard.
     from psutil import process_iter
     from psutil import NoSuchProcess
     from getpass import getuser
     from vyos.defaults import commit_lock
 
     if getuser() != 'root':
         raise OSError('This functions needs to be run as root to return correct results!')
 
     for proc in process_iter():
         try:
             files = proc.open_files()
             if files:
                 for f in files:
                     if f.path == commit_lock:
                         return True
         except NoSuchProcess as err:
             # Process died before we could examine it
             pass
     # Default case
     return False
 
 
 def wait_for_commit_lock():
     """ Not to be used in normal op mode scripts! """
     from time import sleep
     # Very synchronous approach to multiprocessing
     while commit_in_progress():
         sleep(1)
 
 def ask_input(question, default='', numeric_only=False, valid_responses=[]):
     question_out = question
     if default:
         question_out += f' (Default: {default})'
     response = ''
     while True:
         response = input(question_out + ' ').strip()
         if not response and default:
             return default
         if numeric_only:
             if not response.isnumeric():
                 print("Invalid value, try again.")
                 continue
             response = int(response)
         if valid_responses and response not in valid_responses:
             print("Invalid value, try again.")
             continue
         break
     return response
 
 def ask_yes_no(question, default=False) -> bool:
     """Ask a yes/no question via input() and return their answer."""
     from sys import stdout
     default_msg = "[Y/n]" if default else "[y/N]"
     while True:
         try:
             stdout.write("%s %s " % (question, default_msg))
             c = input().lower()
             if c == '':
                 return default
             elif c in ("y", "ye", "yes"):
                 return True
             elif c in ("n", "no"):
                 return False
             else:
                 stdout.write("Please respond with yes/y or no/n\n")
         except EOFError:
             stdout.write("\nPlease respond with yes/y or no/n\n")
 
 def is_admin() -> bool:
     """Look if current user is in sudo group"""
     from getpass import getuser
     from grp import getgrnam
     current_user = getuser()
     (_, _, _, admin_group_members) = getgrnam('sudo')
     return current_user in admin_group_members
 
 
 def mac2eui64(mac, prefix=None):
     """
     Convert a MAC address to a EUI64 address or, with prefix provided, a full
     IPv6 address.
     Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
     """
     import re
     from ipaddress import ip_network
     # http://tools.ietf.org/html/rfc4291#section-2.5.1
     eui64 = re.sub(r'[.:-]', '', mac).lower()
     eui64 = eui64[0:6] + 'fffe' + eui64[6:]
     eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
 
     if prefix is None:
         return ':'.join(re.findall(r'.{4}', eui64))
     else:
         try:
             net = ip_network(prefix, strict=False)
             euil = int('0x{0}'.format(eui64), 16)
             return str(net[euil])
         except:  # pylint: disable=bare-except
             return
 
 def get_half_cpus():
     """ return 1/2 of the numbers of available CPUs """
     cpu = os.cpu_count()
     if cpu > 1:
         cpu /= 2
     return int(cpu)
 
 def check_kmod(k_mod):
     """ Common utility function to load required kernel modules on demand """
     from vyos import ConfigError
     if isinstance(k_mod, str):
         k_mod = k_mod.split()
     for module in k_mod:
         if not os.path.exists(f'/sys/module/{module}'):
             if call(f'modprobe {module}') != 0:
                 raise ConfigError(f'Loading Kernel module {module} failed')
 
 def find_device_file(device):
     """ Recurively search /dev for the given device file and return its full path.
         If no device file was found 'None' is returned """
     from fnmatch import fnmatch
 
     for root, dirs, files in os.walk('/dev'):
         for basename in files:
             if fnmatch(basename, device):
                 return os.path.join(root, basename)
 
     return None
 
 def dict_search(path, dict_object):
     """ Traverse Python dictionary (dict_object) delimited by dot (.).
     Return value of key if found, None otherwise.
 
     This is faster implementation then jmespath.search('foo.bar', dict_object)"""
     if not isinstance(dict_object, dict) or not path:
         return None
 
     parts = path.split('.')
     inside = parts[:-1]
     if not inside:
         if path not in dict_object:
             return None
         return dict_object[path]
     c = dict_object
     for p in parts[:-1]:
         c = c.get(p, {})
     return c.get(parts[-1], None)
 
 def dict_search_args(dict_object, *path):
     # Traverse dictionary using variable arguments
     # Added due to above function not allowing for '.' in the key names
     # Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...)
     if not isinstance(dict_object, dict) or not path:
         return None
 
     for item in path:
         if item not in dict_object:
             return None
         dict_object = dict_object[item]
     return dict_object
 
 def dict_search_recursive(dict_object, key):
     """ Traverse a dictionary recurisvely and return the value of the key
     we are looking for.
 
     Thankfully copied from https://stackoverflow.com/a/19871956
     """
     if isinstance(dict_object, list):
         for i in dict_object:
             for x in dict_search_recursive(i, key):
                yield x
     elif isinstance(dict_object, dict):
         if key in dict_object:
             yield dict_object[key]
         for j in dict_object.values():
             for x in dict_search_recursive(j, key):
                 yield x
 
 def get_interface_config(interface):
     """ Returns the used encapsulation protocol for given interface.
         If interface does not exist, None is returned.
     """
     if not os.path.exists(f'/sys/class/net/{interface}'):
         return None
     from json import loads
     tmp = loads(cmd(f'ip -d -j link show {interface}'))[0]
     return tmp
 
 def get_interface_address(interface):
     """ Returns the used encapsulation protocol for given interface.
         If interface does not exist, None is returned.
     """
     if not os.path.exists(f'/sys/class/net/{interface}'):
         return None
     from json import loads
     tmp = loads(cmd(f'ip -d -j addr show {interface}'))[0]
     return tmp
 
 def get_all_vrfs():
     """ Return a dictionary of all system wide known VRF instances """
     from json import loads
     tmp = loads(cmd('ip -j vrf list'))
     # Result is of type [{"name":"red","table":1000},{"name":"blue","table":2000}]
     # so we will re-arrange it to a more nicer representation:
     # {'red': {'table': 1000}, 'blue': {'table': 2000}}
     data = {}
     for entry in tmp:
         name = entry.pop('name')
         data[name] = entry
     return data
 
 def print_error(str='', end='\n'):
     """
     Print `str` to stderr, terminated with `end`.
     Used for warnings and out-of-band messages to avoid mangling precious
      stdout output.
     """
     sys.stderr.write(str)
     sys.stderr.write(end)
     sys.stderr.flush()
 
 def make_progressbar():
     """
     Make a procedure that takes two arguments `done` and `total` and prints a
      progressbar based on the ratio thereof, whose length is determined by the
      width of the terminal.
     """
     import shutil, math
     col, _ = shutil.get_terminal_size()
     col = max(col - 15, 20)
     def print_progressbar(done, total):
         if done <= total:
             increment = total / col
             length = math.ceil(done / increment)
             percentage = str(math.ceil(100 * done / total)).rjust(3)
             print_error(f'[{length * "#"}{(col - length) * "_"}] {percentage}%', '\r')
             # Print a newline so that the subsequent prints don't overwrite the full bar.
         if done == total:
             print_error()
     return print_progressbar
 
 def make_incremental_progressbar(increment: float):
     """
     Make a generator that displays a progressbar that grows monotonically with
      every iteration.
     First call displays it at 0% and every subsequent iteration displays it
      at `increment` increments where 0.0 < `increment` < 1.0.
     Intended for FTP and HTTP transfers with stateless callbacks.
     """
     print_progressbar = make_progressbar()
     total = 0.0
     while total < 1.0:
         print_progressbar(total, 1.0)
         yield
         total += increment
     print_progressbar(1, 1)
     # Ignore further calls.
     while True:
         yield
 
 def begin(*args):
     """
     Evaluate arguments in order and return the result of the *last* argument.
     For combining multiple expressions in one statement. Useful for lambdas.
     """
     return args[-1]
 
 def begin0(*args):
     """
     Evaluate arguments in order and return the result of the *first* argument.
     For combining multiple expressions in one statement. Useful for lambdas.
     """
     return args[0]
 
 def is_systemd_service_active(service):
     """ Test is a specified systemd service is activated.
     Returns True if service is active, false otherwise.
     Copied from: https://unix.stackexchange.com/a/435317 """
     tmp = cmd(f'systemctl show --value -p ActiveState {service}')
     return bool((tmp == 'active'))
 
 def is_systemd_service_running(service):
     """ Test is a specified systemd service is actually running.
     Returns True if service is running, false otherwise.
     Copied from: https://unix.stackexchange.com/a/435317 """
     tmp = cmd(f'systemctl show --value -p SubState {service}')
     return bool((tmp == 'running'))
 
 def check_port_availability(ipaddress, port, protocol):
     """
     Check if port is available and not used by any service
     Return False if a port is busy or IP address does not exists
     Should be used carefully for services that can start listening
     dynamically, because IP address may be dynamic too
     """
     from socketserver import TCPServer, UDPServer
     from ipaddress import ip_address
 
     # verify arguments
     try:
         ipaddress = ip_address(ipaddress).compressed
     except:
         raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address')
     if port not in range(1, 65536):
         raise ValueError(f'The port number {port} is not in the 1-65535 range')
     if protocol not in ['tcp', 'udp']:
         raise ValueError(
             f'The protocol {protocol} is not supported. Only tcp and udp are allowed'
         )
 
     # check port availability
     try:
         if protocol == 'tcp':
             server = TCPServer((ipaddress, port), None, bind_and_activate=True)
         if protocol == 'udp':
             server = UDPServer((ipaddress, port), None, bind_and_activate=True)
         server.server_close()
         return True
     except:
         return False
 
 def install_into_config(conf, config_paths, override_prompt=True):
     # Allows op-mode scripts to install values if called from an active config session
     # config_paths: dict of config paths
     # override_prompt: if True, user will be prompted before existing nodes are overwritten
 
     if not config_paths:
         return None
 
     from vyos.config import Config
 
     if not Config().in_session():
         print('You are not in configure mode, commands to install manually from configure mode:')
         for path in config_paths:
             print(f'set {path}')
         return None
 
     count = 0
 
     for path in config_paths:
         if override_prompt and conf.exists(path) and not conf.is_multi(path):
             if not ask_yes_no(f'Config node "{node}" already exists. Do you want to overwrite it?'):
                 continue
 
         cmd(f'/opt/vyatta/sbin/my_set {path}')
         count += 1
 
     if count > 0:
         print(f'{count} value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.')
 
 def is_wwan_connected(interface):
     """ Determine if a given WWAN interface, e.g. wwan0 is connected to the
     carrier network or not """
     import json
 
     if not interface.startswith('wwan'):
         raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
 
     modem = interface.lstrip('wwan')
 
     tmp = cmd(f'mmcli --modem {modem} --output-json')
     tmp = json.loads(tmp)
 
     # return True/False if interface is in connected state
     return dict_search('modem.generic.state', tmp) == 'connected'
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 25dcdf7c6..6c6367045 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -1,600 +1,604 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2018-2021 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
 
 from sys import exit
 
 from vyos.config import Config
 from vyos.configverify import verify_vrf
 from vyos.snmpv3_hashgen import plaintext_to_md5
 from vyos.snmpv3_hashgen import plaintext_to_sha1
 from vyos.snmpv3_hashgen import random
 from vyos.template import render
 from vyos.template import is_ipv4
 from vyos.util import call
 from vyos.util import chmod_755
 from vyos.validate import is_addr_assigned
 from vyos.version import get_version_data
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 config_file_client  = r'/etc/snmp/snmp.conf'
 config_file_daemon  = r'/etc/snmp/snmpd.conf'
 config_file_access  = r'/usr/share/snmp/snmpd.conf'
 config_file_user    = r'/var/lib/snmp/snmpd.conf'
 default_script_dir  = r'/config/user-data/'
 systemd_override    = r'/etc/systemd/system/snmpd.service.d/override.conf'
 
 # SNMP OIDs used to mark auth/priv type
 OIDs = {
     'md5' : '.1.3.6.1.6.3.10.1.1.2',
     'sha' : '.1.3.6.1.6.3.10.1.1.3',
     'aes' : '.1.3.6.1.6.3.10.1.2.4',
     'des' : '.1.3.6.1.6.3.10.1.2.2',
     'none': '.1.3.6.1.6.3.10.1.2.1'
 }
 
 default_config_data = {
     'listen_on': [],
     'listen_address': [],
     'ipv6_enabled': 'True',
     'communities': [],
     'smux_peers': [],
     'location' : '',
     'protocol' : 'udp',
     'description' : '',
     'contact' : '',
     'route_table': 'False',
     'trap_source': '',
     'trap_targets': [],
     'vyos_user': '',
     'vyos_user_pass': '',
     'version': '',
     'v3_enabled': 'False',
     'v3_engineid': '',
     'v3_groups': [],
     'v3_traps': [],
     'v3_users': [],
     'v3_views': [],
     'script_ext': []
 }
 
 def rmfile(file):
     if os.path.isfile(file):
         os.unlink(file)
 
 def get_config():
     snmp = default_config_data
     conf = Config()
     if not conf.exists('service snmp'):
         return None
     else:
         if conf.exists('system ipv6 disable'):
             snmp['ipv6_enabled'] = False
 
         conf.set_level('service snmp')
 
     version_data = get_version_data()
     snmp['version'] = version_data['version']
 
     # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
     snmp['vyos_user'] = 'vyos' + random(8)
     snmp['vyos_user_pass'] = random(16)
 
     if conf.exists('community'):
         for name in conf.list_nodes('community'):
             community = {
                 'name': name,
                 'authorization': 'ro',
                 'network_v4': [],
                 'network_v6': [],
                 'has_source' : False
             }
 
             if conf.exists('community {0} authorization'.format(name)):
                 community['authorization'] = conf.return_value('community {0} authorization'.format(name))
 
             # Subnet of SNMP client(s) allowed to contact system
             if conf.exists('community {0} network'.format(name)):
                 for addr in conf.return_values('community {0} network'.format(name)):
                     if is_ipv4(addr):
                         community['network_v4'].append(addr)
                     else:
                         community['network_v6'].append(addr)
 
             # IP address of SNMP client allowed to contact system
             if conf.exists('community {0} client'.format(name)):
                 for addr in conf.return_values('community {0} client'.format(name)):
                     if is_ipv4(addr):
                         community['network_v4'].append(addr)
                     else:
                         community['network_v6'].append(addr)
 
             if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0):
                  community['has_source'] = True
 
             snmp['communities'].append(community)
 
     if conf.exists('contact'):
         snmp['contact'] = conf.return_value('contact')
 
     if conf.exists('description'):
         snmp['description'] = conf.return_value('description')
 
     if conf.exists('listen-address'):
         for addr in conf.list_nodes('listen-address'):
             port = '161'
             if conf.exists('listen-address {0} port'.format(addr)):
                 port = conf.return_value('listen-address {0} port'.format(addr))
 
             snmp['listen_address'].append((addr, port))
 
         # Always listen on localhost if an explicit address has been configured
         # This is a safety measure to not end up with invalid listen addresses
         # that are not configured on this system. See https://phabricator.vyos.net/T850
         if not '127.0.0.1' in conf.list_nodes('listen-address'):
             snmp['listen_address'].append(('127.0.0.1', '161'))
 
         if not '::1' in conf.list_nodes('listen-address'):
             snmp['listen_address'].append(('::1', '161'))
 
     if conf.exists('location'):
         snmp['location'] = conf.return_value('location')
 
     if conf.exists('protocol'):
         snmp['protocol'] = conf.return_value('protocol')
 
     if conf.exists('smux-peer'):
         snmp['smux_peers'] = conf.return_values('smux-peer')
 
     if conf.exists('trap-source'):
         snmp['trap_source'] = conf.return_value('trap-source')
 
     if conf.exists('trap-target'):
         for target in conf.list_nodes('trap-target'):
             trap_tgt = {
                 'target': target,
                 'community': '',
                 'port': ''
             }
 
             if conf.exists('trap-target {0} community'.format(target)):
                trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))
 
             if conf.exists('trap-target {0} port'.format(target)):
                 trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))
 
             snmp['trap_targets'].append(trap_tgt)
 
     if conf.exists('script-extensions'):
         for extname in conf.list_nodes('script-extensions extension-name'):
             conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
             # if script has not absolute path, use pre configured path
             if "/" not in conf_script:
                 conf_script = default_script_dir + conf_script
 
             extension = {
                 'name': extname,
                 'script' : conf_script
             }
 
             snmp['script_ext'].append(extension)
 
     if conf.exists('oid-enable route-table'):
         snmp['route_table'] = True
 
     if conf.exists('vrf'):
         # Append key to dict but don't place it in the default dictionary.
         # This is required to make the override.conf.tmpl work until we
         # migrate to get_config_dict().
         snmp['vrf'] = conf.return_value('vrf')
 
 
     #########################################################################
     #                ____  _   _ __  __ ____          _____                 #
     #               / ___|| \ | |  \/  |  _ \  __   _|___ /                 #
     #               \___ \|  \| | |\/| | |_) | \ \ / / |_ \                 #
     #                ___) | |\  | |  | |  __/   \ V / ___) |                #
     #               |____/|_| \_|_|  |_|_|       \_/ |____/                 #
     #                                                                       #
     #     now take care about the fancy SNMP v3 stuff, or bail out eraly    #
     #########################################################################
     if not conf.exists('v3'):
         return snmp
     else:
         snmp['v3_enabled'] = True
 
     # 'set service snmp v3 engineid'
     if conf.exists('v3 engineid'):
         snmp['v3_engineid'] = conf.return_value('v3 engineid')
 
     # 'set service snmp v3 group'
     if conf.exists('v3 group'):
         for group in conf.list_nodes('v3 group'):
             v3_group = {
                 'name': group,
                 'mode': 'ro',
                 'seclevel': 'auth',
                 'view': ''
             }
 
             if conf.exists('v3 group {0} mode'.format(group)):
                 v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))
 
             if conf.exists('v3 group {0} seclevel'.format(group)):
                 v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))
 
             if conf.exists('v3 group {0} view'.format(group)):
                 v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))
 
             snmp['v3_groups'].append(v3_group)
 
     # 'set service snmp v3 trap-target'
     if conf.exists('v3 trap-target'):
         for trap in conf.list_nodes('v3 trap-target'):
             trap_cfg = {
                 'ipAddr': trap,
                 'secName': '',
                 'authProtocol': 'md5',
                 'authPassword': '',
                 'authMasterKey': '',
                 'privProtocol': 'des',
                 'privPassword': '',
                 'privMasterKey': '',
                 'ipProto': 'udp',
                 'ipPort': '162',
                 'type': '',
                 'secLevel': 'noAuthNoPriv'
             }
 
             if conf.exists('v3 trap-target {0} user'.format(trap)):
                 # Set the securityName used for authenticated SNMPv3 messages.
                 trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
 
             if conf.exists('v3 trap-target {0} auth type'.format(trap)):
                 # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
                 # cmdline option '-a'
                 trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))
 
             if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)):
                 # Set the authentication pass phrase used for authenticated SNMPv3 messages.
                 # cmdline option '-A'
                 trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap))
 
             if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)):
                 # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
                 # cmdline option '-3m'
                 trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap))
 
             if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
                 # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
                 # cmdline option '-x'
                 trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))
 
             if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)):
                 # Set the privacy pass phrase used for encrypted SNMPv3 messages.
                 # cmdline option '-X'
                 trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap))
 
             if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)):
                 # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
                 # cmdline option '-3M'
                 trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap))
 
             if conf.exists('v3 trap-target {0} protocol'.format(trap)):
                 trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))
 
             if conf.exists('v3 trap-target {0} port'.format(trap)):
                 trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))
 
             if conf.exists('v3 trap-target {0} type'.format(trap)):
                 trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap))
 
             # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
             # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
             if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
                 if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
                     trap_cfg['secLevel'] = 'authPriv'
                 else:
                     trap_cfg['secLevel'] = 'authNoPriv'
 
             snmp['v3_traps'].append(trap_cfg)
 
     # 'set service snmp v3 user'
     if conf.exists('v3 user'):
         for user in conf.list_nodes('v3 user'):
             user_cfg = {
                 'name': user,
                 'authMasterKey': '',
                 'authPassword': '',
                 'authProtocol': 'md5',
                 'authOID': 'none',
                 'group': '',
                 'mode': 'ro',
                 'privMasterKey': '',
                 'privPassword': '',
                 'privOID': '',
                 'privProtocol': 'des'
             }
 
             # v3 user {0} auth
             if conf.exists('v3 user {0} auth encrypted-password'.format(user)):
                 user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user))
 
             if conf.exists('v3 user {0} auth plaintext-password'.format(user)):
                 user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user))
 
             # load default value
             type = user_cfg['authProtocol']
             if conf.exists('v3 user {0} auth type'.format(user)):
                 type = conf.return_value('v3 user {0} auth type'.format(user))
 
             # (re-)update with either default value or value from CLI
             user_cfg['authProtocol'] = type
             user_cfg['authOID'] = OIDs[type]
 
             # v3 user {0} group
             if conf.exists('v3 user {0} group'.format(user)):
                 user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
 
             # v3 user {0} mode
             if conf.exists('v3 user {0} mode'.format(user)):
                 user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
 
             # v3 user {0} privacy
             if conf.exists('v3 user {0} privacy encrypted-password'.format(user)):
                 user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user))
 
             if conf.exists('v3 user {0} privacy plaintext-password'.format(user)):
                 user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user))
 
             # load default value
             type = user_cfg['privProtocol']
             if conf.exists('v3 user {0} privacy type'.format(user)):
                 type = conf.return_value('v3 user {0} privacy type'.format(user))
 
             # (re-)update with either default value or value from CLI
             user_cfg['privProtocol'] = type
             user_cfg['privOID'] = OIDs[type]
 
             snmp['v3_users'].append(user_cfg)
 
     # 'set service snmp v3 view'
     if conf.exists('v3 view'):
         for view in conf.list_nodes('v3 view'):
             view_cfg = {
                 'name': view,
                 'oids': []
             }
 
             if conf.exists('v3 view {0} oid'.format(view)):
                 for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
                     oid_cfg = {
                         'oid': oid
                     }
                     view_cfg['oids'].append(oid_cfg)
             snmp['v3_views'].append(view_cfg)
 
     return snmp
 
 def verify(snmp):
     if snmp is None:
         # we can not delete SNMP when LLDP is configured with SNMP
         conf = Config()
         if conf.exists('service lldp snmp enable'):
             raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
 
         return None
 
     ### check if the configured script actually exist
     if snmp['script_ext']:
         for ext in snmp['script_ext']:
             if not os.path.isfile(ext['script']):
                 print ("WARNING: script: {} doesn't exist".format(ext['script']))
             else:
                 chmod_755(ext['script'])
 
     for listen in snmp['listen_address']:
         addr = listen[0]
         port = listen[1]
         protocol = snmp['protocol']
 
         tmp = None
         if is_ipv4(addr):
             # example: udp:127.0.0.1:161
             tmp = f'{protocol}:{addr}:{port}'
         elif snmp['ipv6_enabled']:
             # example: udp6:[::1]:161
             tmp = f'{protocol}6:[{addr}]:{port}'
 
         # We only wan't to configure addresses that exist on the system.
         # Hint the user if they don't exist
         if is_addr_assigned(addr):
             if tmp: snmp['listen_on'].append(tmp)
         else:
             print(f'WARNING: SNMP listen address {addr} not configured!')
 
     verify_vrf(snmp)
 
     # bail out early if SNMP v3 is not configured
     if not snmp['v3_enabled']:
         return None
 
     if 'v3_groups' in snmp.keys():
         for group in snmp['v3_groups']:
             #
             # A view must exist prior to mapping it into a group
             #
             if 'view' in group.keys():
                 error = True
                 if 'v3_views' in snmp.keys():
                     for view in snmp['v3_views']:
                         if view['name'] == group['view']:
                             error = False
                 if error:
                     raise ConfigError('You must create view "{0}" first'.format(group['view']))
             else:
                 raise ConfigError('"view" must be specified')
 
             if not 'mode' in group.keys():
                 raise ConfigError('"mode" must be specified')
 
             if not 'seclevel' in group.keys():
                 raise ConfigError('"seclevel" must be specified')
 
     if 'v3_traps' in snmp.keys():
         for trap in snmp['v3_traps']:
             if trap['authPassword'] and trap['authMasterKey']:
                 raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth')
 
             if trap['authPassword'] == '' and trap['authMasterKey'] == '':
                 raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth')
 
             if trap['privPassword'] and trap['privMasterKey']:
                 raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy')
 
             if trap['privPassword'] == '' and trap['privMasterKey'] == '':
                 raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy')
 
             if not 'type' in trap.keys():
                 raise ConfigError('v3 trap: "type" must be specified')
 
             if not 'authPassword' and 'authMasterKey' in trap.keys():
                 raise ConfigError('v3 trap: "auth" must be specified')
 
             if not 'authProtocol' in trap.keys():
                 raise ConfigError('v3 trap: "protocol" must be specified')
 
             if not 'privPassword' and 'privMasterKey' in trap.keys():
                 raise ConfigError('v3 trap: "user" must be specified')
 
     if 'v3_users' in snmp.keys():
         for user in snmp['v3_users']:
             #
             # Group must exist prior to mapping it into a group
             # seclevel will be extracted from group
             #
             if user['group']:
                 error = True
                 if 'v3_groups' in snmp.keys():
                     for group in snmp['v3_groups']:
                         if group['name'] == user['group']:
                             seclevel = group['seclevel']
                             error = False
 
                 if error:
                     raise ConfigError('You must create group "{0}" first'.format(user['group']))
 
             # Depending on the configured security level the user has to provide additional info
             if (not user['authPassword'] and not user['authMasterKey']):
                 raise ConfigError('Must specify encrypted-password or plaintext-key for user auth')
 
             if user['privPassword'] == '' and user['privMasterKey'] == '':
                 raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy')
 
             if user['mode'] == '':
                 raise ConfigError('Must specify user mode ro/rw')
 
     if 'v3_views' in snmp.keys():
         for view in snmp['v3_views']:
             if not view['oids']:
                 raise ConfigError('Must configure an oid')
 
     return None
 
 def generate(snmp):
     #
     # As we are manipulating the snmpd user database we have to stop it first!
     # This is even save if service is going to be removed
     call('systemctl stop snmpd.service')
     config_files = [config_file_client, config_file_daemon, config_file_access,
                     config_file_user, systemd_override]
     for file in config_files:
         rmfile(file)
 
     if not snmp:
         return None
 
     if 'v3_users' in snmp.keys():
         # net-snmp is now regenerating the configuration file in the background
         # thus we need to re-open and re-read the file as the content changed.
         # After that we can no read the encrypted password from the config and
         # replace the CLI plaintext password with its encrypted version.
         os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos"
 
         for user in snmp['v3_users']:
             if user['authProtocol'] == 'sha':
                 hash = plaintext_to_sha1
             else:
                 hash = plaintext_to_md5
 
             if user['authPassword']:
                 user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid'])
                 user['authPassword'] = ''
 
                 call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user))
                 call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user))
 
             if user['privPassword']:
                 user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid'])
                 user['privPassword'] = ''
 
                 call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user))
                 call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user))
 
     # Write client config file
     render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp)
     # Write server config file
     render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp)
     # Write access rights config file
     render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
     # Write access rights config file
     render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
     # Write daemon configuration file
     render(systemd_override, 'snmp/override.conf.tmpl', snmp)
 
     return None
 
 def apply(snmp):
     # Always reload systemd manager configuration
     call('systemctl daemon-reload')
 
     if not snmp:
         return None
 
     # start SNMP daemon
     call('systemctl restart snmpd.service')
 
     # Enable AgentX in FRR
     # This should be done for each daemon individually because common command
     # works only if all the daemons started with SNMP support
-    frr_daemons_list = ['bgpd', 'ospf6d', 'ospfd', 'ripd', 'zebra']
+    frr_daemons_list = [
+        'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra'
+    ]
     for frr_daemon in frr_daemons_list:
-        call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null')
+        call(
+            f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null'
+        )
 
     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/conf_mode/system_frr.py b/src/conf_mode/system_frr.py
index 0d0b37e00..1af0055f6 100755
--- a/src/conf_mode/system_frr.py
+++ b/src/conf_mode/system_frr.py
@@ -1,91 +1,91 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2021 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/>.
 
 from pathlib import Path
 from sys import exit
 
 from vyos import ConfigError
 from vyos import airbag
 from vyos.config import Config
 from vyos.logger import syslog
 from vyos.template import render_to_string
 from vyos.util import read_file, write_file, run
 airbag.enable()
 
 # path to daemons config and config status files
 config_file = '/etc/frr/daemons'
 vyos_status_file = '/tmp/vyos-config-status'
 # path to watchfrr for FRR control
 watchfrr = '/usr/lib/frr/watchfrr.sh'
 
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
 
     base = ['system', 'frr']
     frr_config = conf.get_config_dict(base, get_first_key=True)
 
     return frr_config
 
 
 def verify(frr_config):
     # Nothing to verify here
     pass
 
 
 def generate(frr_config):
     # read daemons config file
-    daemons_config_current = read_file(config_file, strip_end=False)
+    daemons_config_current = read_file(config_file)
     # generate new config file
     daemons_config_new = render_to_string('frr/daemons.frr.tmpl', frr_config)
     # update configuration file if this is necessary
     if daemons_config_new != daemons_config_current:
         syslog.warning('FRR daemons configuration file need to be changed')
         write_file(config_file, daemons_config_new)
         frr_config['config_file_changed'] = True
 
 
 def apply(frr_config):
     # check if this is initial commit during boot or intiated by CLI
     # if the file exists, this must be CLI commit
     commit_type_cli = Path(vyos_status_file).exists()
     # display warning to user
     if commit_type_cli and frr_config.get('config_file_changed'):
         # Since FRR restart is not safe thing, better to give
         # control over this to users
         print('''
         You need to reboot a router (preferred) or restart FRR
         to apply changes in modules settings
         ''')
     # restart FRR automatically. DUring the initial boot this should be
     # safe in most cases
     if not commit_type_cli and frr_config.get('config_file_changed'):
         syslog.warning('Restarting FRR to apply changes in modules')
         run(f'{watchfrr} restart')
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)