]> git.proxmox.com Git - mirror_frr.git/commitdiff
Merge pull request #8167 from LabNConsulting/chopps/tests-add-gdb
authorDonald Sharp <sharpd@cumulusnetworks.com>
Thu, 25 Mar 2021 14:27:50 +0000 (10:27 -0400)
committerGitHub <noreply@github.com>
Thu, 25 Mar 2021 14:27:50 +0000 (10:27 -0400)
tests: add option for auto-launching gdb

1  2 
doc/developer/topotests.rst
tests/topotests/lib/topotest.py

index 4f58ee335bffc35b0c5d20ce8e32fdd029278b3e,e684b9c8a865e27def6020077e91932e21032b1b..2a6d2dda343c251d88a085ef6a6ff6e26fcd34d2
@@@ -60,7 -60,6 +60,7 @@@ following steps will get you there on U
  
  .. code:: shell
          
 +   apt install libsnmp-dev
     apt install snmpd snmp
     apt install snmp-mibs-downloader
     download-mibs
@@@ -233,6 -232,85 +233,85 @@@ for ``master`` branch
  
  and create ``frr`` user and ``frrvty`` group as shown above.
  
+ Debugging Topotest Failures
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ For the below debugging options which launch programs, if the topotest is run
+ within screen_ or tmux_, ``gdb``, the shell or ``vtysh`` will be launched using
+ that windowing program, otherwise mininet's ``xterm`` functionality will be used
+ to launch the given program.
+ If you wish to force the use of ``xterm`` rather than ``tmux`` or ``screen``, or
+ wish to use ``gnome-terminal`` instead of ``xterm``, set the environment
+ variable ``FRR_TOPO_TERMINAL`` to either ``xterm`` or ``gnome-terminal``.
+ .. _screen: https://www.gnu.org/software/screen/
+ .. _tmux: https://github.com/tmux/tmux/wiki
+ Spawning ``vtysh`` or Shells on Routers
+ """""""""""""""""""""""""""""""""""""""
+ Topotest can automatically launch a shell or ``vtysh`` for any or all routers in
+ a test. This is enabled by specifying 1 of 2 CLI arguments ``--shell`` or
+ ``--vtysh``. Both of these options can be set to a single router value, multiple
+ comma-seperated values, or ``all``.
+ When either of these options are specified topotest will pause after each test
+ to allow for inspection of the router state.
+ Here's an example of launching ``vtysh`` on routers ``rt1`` and ``rt2``.
+ .. code:: shell
+    pytest --vtysh=rt1,rt2 all-protocol-startup
+ Spawning Mininet CLI, ``vtysh`` or Shells on Routers on Test Failure
+ """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+ Similar to the previous section one can have ``vtysh`` or a shell launched on
+ routers, but in this case only when a test fails. To launch the given process on
+ each router after a test failure specify one of ``--shell-on-error`` or
+ ``--vtysh-on-error``.
+ Here's an example of having ``vtysh`` launched on test failure.
+ .. code:: shell
+    pytest --vtysh-on-error all-protocol-startup
+ Additionally, one can have the mininet CLI invoked on test failures by
+ specifying the ``--mininet-on-error`` CLI option as shown in the example below.
+ .. code:: shell
+    pytest --mininet-on-error all-protocol-startup
+ Debugging with GDB
+ """"""""""""""""""
+ Topotest can automatically launch any daemon with ``gdb``, possibly setting
+ breakpoints for any test run. This is enabled by specifying 1 or 2 CLI arguments
+ ``--gdb-routers`` and ``--gdb-daemons``. Additionally ``--gdb-breakpoints`` can
+ be used to automatically set breakpoints in the launched ``gdb`` processes.
+ Each of these options can be set to a single value, multiple comma-seperated
+ values, or ``all``. If ``--gdb-routers`` is empty but ``--gdb_daemons`` is set
+ then the given daemons will be launched in ``gdb`` on all routers in the test.
+ Likewise if ``--gdb_routers`` is set, but ``--gdb_daemons`` is empty then all
+ daemons on the given routers will be launched in ``gdb``.
+ Here's an example of launching ``zebra`` and ``bgpd`` inside ``gdb`` on router
+ ``r1`` with a breakpoint set on ``nb_config_diff``
+ .. code:: shell
+    pytest --gdb-routers=r1 \
+           --gdb-daemons=bgpd,zebra \
+           --gdb-breakpoints=nb_config_diff \
+           all-protocol-startup
  .. _topotests_docker:
  
  Running Tests with Docker
index 70b2cfd648c2de67383d5f203599b30668db850b,7f768f5b89b873380332e07a8a4830664bdd25bf..104b21507877435c46fc603a3464a32d5bf0a36a
@@@ -50,7 -50,9 +50,9 @@@ from mininet.node import Node, OVSSwitc
  from mininet.log import setLogLevel, info
  from mininet.cli import CLI
  from mininet.link import Intf
+ from mininet.term import makeTerm
  
+ g_extra_config = {}
  
  def gdb_core(obj, daemon, corefiles):
      gdbcmds = """
@@@ -516,44 -518,6 +518,44 @@@ def normalize_text(text)
      return text
  
  
 +def is_linux():
 +    """
 +    Parses unix name output to check if running on GNU/Linux.
 +
 +    Returns True if running on Linux, returns False otherwise.
 +    """
 +
 +    if os.uname()[0] == "Linux":
 +        return True
 +    return False
 +
 +
 +def iproute2_is_vrf_capable():
 +    """
 +    Checks if the iproute2 version installed on the system is capable of
 +    handling VRFs by interpreting the output of the 'ip' utility found in PATH.
 +
 +    Returns True if capability can be detected, returns False otherwise.
 +    """
 +
 +    if is_linux():
 +        try:
 +            subp = subprocess.Popen(
 +                ["ip", "route", "show", "vrf"],
 +                stdout=subprocess.PIPE,
 +                stderr=subprocess.PIPE,
 +                stdin=subprocess.PIPE,
 +                encoding="utf-8"
 +            )
 +            iproute2_err = subp.communicate()[1].splitlines()[0].split()[0]
 +
 +            if iproute2_err != "Error:":
 +                return True
 +        except Exception:
 +            pass
 +    return False
 +
 +
  def module_present_linux(module, load):
      """
      Returns whether `module` is present.
@@@ -1341,6 -1305,37 +1343,37 @@@ class Router(Node)
              logger.info("No daemon {} known".format(daemon))
          # print "Daemons after:", self.daemons
  
+     # Run a command in a new window (gnome-terminal, screen, tmux, xterm)
+     def runInWindow(self, cmd, title=None):
+         topo_terminal = os.getenv("FRR_TOPO_TERMINAL")
+         if topo_terminal or (
+                 "TMUX" not in os.environ and "STY" not in os.environ
+         ):
+             term = topo_terminal if topo_terminal else "xterm"
+             makeTerm(
+                 self,
+                 title=title if title else cmd,
+                 term=term,
+                 cmd=cmd)
+         else:
+             nscmd = "sudo nsenter -m -n -t {} {}".format(self.pid, cmd)
+             if "TMUX" in os.environ:
+                 self.cmd("tmux select-layout main-horizontal")
+                 wcmd = "tmux split-window -h"
+                 cmd = "{} {}".format(wcmd, nscmd)
+             elif "STY" in os.environ:
+                 if os.path.exists(
+                         "/run/screen/S-{}/{}".format(
+                             os.environ['USER'], os.environ['STY']
+                         )
+                 ):
+                     wcmd = "screen"
+                 else:
+                     wcmd = "sudo -u {} screen".format(os.environ["SUDO_USER"])
+                 cmd = "{} {}".format(wcmd, nscmd)
+             self.cmd(cmd)
      def startRouter(self, tgen=None):
          # Disable integrated-vtysh-config
          self.cmd(
                  return "LDP/MPLS Tests need mpls kernel modules"
          self.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels")
  
+         shell_routers = g_extra_config["shell"]
+         if "all" in shell_routers or self.name in shell_routers:
+             self.runInWindow(os.getenv("SHELL", "bash"))
+         vtysh_routers = g_extra_config["vtysh"]
+         if "all" in vtysh_routers or self.name in vtysh_routers:
+             self.runInWindow("vtysh")
          if self.daemons["eigrpd"] == 1:
              eigrpd_path = os.path.join(self.daemondir, "eigrpd")
              if not os.path.isfile(eigrpd_path):
      def startRouterDaemons(self, daemons=None):
          "Starts all FRR daemons for this router."
  
+         gdb_breakpoints =  g_extra_config["gdb_breakpoints"]
+         gdb_daemons = g_extra_config["gdb_daemons"]
+         gdb_routers = g_extra_config["gdb_routers"]
          bundle_data = ""
  
          if os.path.exists("/etc/frr/support_bundle_commands.conf"):
          # If `daemons` was specified then some upper API called us with
          # specific daemons, otherwise just use our own configuration.
          daemons_list = []
-         if daemons != None:
+         if daemons is not None:
              daemons_list = daemons
          else:
              # Append all daemons configured.
                  if self.daemons[daemon] == 1:
                      daemons_list.append(daemon)
  
-         # Start Zebra first
-         if "zebra" in daemons_list:
-             zebra_path = os.path.join(self.daemondir, "zebra")
-             zebra_option = self.daemons_options["zebra"]
-             self.cmd(
-                 "ASAN_OPTIONS=log_path=zebra.asan {0} {1} --log file:zebra.log --log-level debug -s 90000000 -d > zebra.out 2> zebra.err".format(
-                     zebra_path, zebra_option
+         def start_daemon(daemon, extra_opts=None):
+             daemon_opts = self.daemons_options.get(daemon, "")
+             rediropt = " > {0}.out 2> {0}.err".format(daemon)
+             if daemon == "snmpd":
+                 binary = "/usr/sbin/snmpd"
+                 cmdenv = ""
+                 cmdopt = "{} -C -c /etc/frr/snmpd.conf -p ".format(
+                     daemon_opts
+                 ) + "/var/run/{}/snmpd.pid -x /etc/frr/agentx".format(self.routertype)
+             else:
+                 binary = os.path.join(self.daemondir, daemon)
+                 cmdenv = "ASAN_OPTIONS=log_path={0}.asan".format(daemon)
+                 cmdopt = "{} --log file:{}.log --log-level debug".format(
+                     daemon_opts, daemon
                  )
-             )
-             logger.debug("{}: {} zebra started".format(self, self.routertype))
+             if extra_opts:
+                 cmdopt += " " + extra_opts
+             if (
+                 (gdb_routers or gdb_daemons)
+                 and (not gdb_routers
+                      or self.name in gdb_routers
+                      or "all" in gdb_routers)
+                 and (not gdb_daemons
+                      or daemon in gdb_daemons
+                      or "all" in gdb_daemons)
+             ):
+                 if daemon == "snmpd":
+                     cmdopt += " -f "
+                 cmdopt += rediropt
+                 gdbcmd = "sudo -E gdb " + binary
+                 if gdb_breakpoints:
+                     gdbcmd += " -ex 'set breakpoint pending on'"
+                 for bp in gdb_breakpoints:
+                     gdbcmd += " -ex 'b {}'".format(bp)
+                 gdbcmd += " -ex 'run {}'".format(cmdopt)
+                 self.runInWindow(gdbcmd, daemon)
+             else:
+                 if daemon != "snmpd":
+                     cmdopt += " -d "
+                 cmdopt += rediropt
+                 self.cmd(" ".join([cmdenv, binary, cmdopt]))
+             logger.info("{}: {} {} started".format(self, self.routertype, daemon))
  
-             # Remove `zebra` so we don't attempt to start it again.
+         # Start Zebra first
+         if "zebra" in daemons_list:
+             start_daemon("zebra", "-s 90000000")
              while "zebra" in daemons_list:
                  daemons_list.remove("zebra")
  
          # Start staticd next if required
          if "staticd" in daemons_list:
-             staticd_path = os.path.join(self.daemondir, "staticd")
-             staticd_option = self.daemons_options["staticd"]
-             self.cmd(
-                 "ASAN_OPTIONS=log_path=staticd.asan {0} {1} --log file:staticd.log --log-level debug -d > staticd.out 2> staticd.err".format(
-                     staticd_path, staticd_option
-                 )
-             )
-             logger.debug("{}: {} staticd started".format(self, self.routertype))
-             # Remove `staticd` so we don't attempt to start it again.
+             start_daemon("staticd")
              while "staticd" in daemons_list:
                  daemons_list.remove("staticd")
  
          if "snmpd" in daemons_list:
-             snmpd_path = "/usr/sbin/snmpd"
-             snmpd_option = self.daemons_options["snmpd"]
-             self.cmd(
-                 "{0} {1} -C -c /etc/frr/snmpd.conf -p /var/run/{2}/snmpd.pid -x /etc/frr/agentx > snmpd.out 2> snmpd.err".format(
-                     snmpd_path, snmpd_option, self.routertype
-                 )
-             )
-             logger.info("{}: {} snmpd started".format(self, self.routertype))
-             # Remove `snmpd` so we don't attempt to start it again.
+             start_daemon("snmpd")
              while "snmpd" in daemons_list:
                  daemons_list.remove("snmpd")
  
  
          # Now start all the other daemons
          for daemon in daemons_list:
-             # Skip disabled daemons and zebra
              if self.daemons[daemon] == 0:
                  continue
-             daemon_path = os.path.join(self.daemondir, daemon)
-             self.cmd(
-                 "ASAN_OPTIONS=log_path={2}.asan {0} {1} --log file:{2}.log --log-level debug -d > {2}.out 2> {2}.err".format(
-                     daemon_path, self.daemons_options.get(daemon, ""), daemon
-                 )
-             )
-             logger.debug("{}: {} {} started".format(self, self.routertype, daemon))
+             start_daemon(daemon)
  
          # Check if daemons are running.
          rundaemons = self.cmd("ls -1 /var/run/%s/*.pid" % self.routertype)