diff --git a/doc/user/frr-reload.rst b/doc/user/frr-reload.rst index 9348b876a4..bd295dbbad 100644 --- a/doc/user/frr-reload.rst +++ b/doc/user/frr-reload.rst @@ -4,9 +4,38 @@ The frr-reload.py script ======================== - +The ``frr-reload.py`` script attempts to update the configuration of running +daemons. It takes as argument the path of the configuration file that we want +to apply. The script will attempt to retrieve the running configuration from +daemons, calculate the delta between that config and the intended one, and +execute the required sequence of vtysh commands to enforce the changes. Options ------- -There are several options that control the behavior of ``frr-reload``. +There are several options that control the behavior of ``frr-reload``: + +* ``--input INPUT``: uses the specified input file as the running configuration + instead of retrieving it from a ``show running-config`` in vtysh +* ``--reload``: applies the configuration delta to the daemons. Either this or + ``--test`` MUST be specified. +* ``--test``: only outputs the configuration delta, without enforcing it. + Either this or ``--reload`` MUST be specified. +* ``--debug``: enable debug messages +* ``--stdout``: print output to stdout +* ``--bindir BINDIR``: path to the vtysh executable +* ``--confdir CONFDIR``: path to the existing daemon config files +* ``--rundir RUNDIR``: path to a folder to be used to write the temporary files + needed by the script to do its job. The script should have write access to it +* ``--daemon DAEMON``: by default ``frr-reload.py`` assumes that we are using + integrated config and attempting to update the configuration for all daemons. + If this is not the case, e.g. each daemon has its individual config file, + then the delta can only be computed on a per-daemon basis. This option allows + the user to specify the daemon for which the config is intended. DAEMON + should be one of the keywords allowed in vtysh as an option for ``show + running-config``. +* ``--vty_socket VTY_SOCKET``: the socket to be used by vtysh to connect to the + running daemons. +* ``--overwrite``: overwrite the existing daemon config file with the new + config after the delta has been applied. The file name will be ``frr.conf`` + for integrate config, or ``DAEMON.conf`` when using per-daemon config files. diff --git a/tools/frr-reload.py b/tools/frr-reload.py index c10eb487e6..bdba65ee2f 100755 --- a/tools/frr-reload.py +++ b/tools/frr-reload.py @@ -32,7 +32,7 @@ from __future__ import print_function, unicode_literals import argparse import copy import logging -import os +import os, os.path import random import re import string @@ -59,9 +59,111 @@ else: log = logging.getLogger(__name__) -class VtyshMarkException(Exception): +class VtyshException(Exception): pass +class Vtysh(object): + def __init__(self, bindir=None, confdir=None, sockdir=None): + self.bindir = bindir + self.confdir = confdir + self.common_args = [os.path.join(bindir or '', 'vtysh')] + if confdir: + self.common_args.extend(['--config_dir', confdir]) + if sockdir: + self.common_args.extend(['--vty_socket', sockdir]) + + def _call(self, args, stdin=None, stdout=None, stderr=None): + kwargs = {} + if stdin is not None: + kwargs['stdin'] = stdin + if stdout is not None: + kwargs['stdout'] = stdout + if stderr is not None: + kwargs['stderr'] = stderr + return subprocess.Popen(self.common_args + args, **kwargs) + + def _call_cmd(self, command, stdin=None, stdout=None, stderr=None): + if isinstance(command, list): + args = [item for sub in command for item in ['-c', sub]] + else: + args = ['-c', command] + return self._call(args, stdin, stdout, stderr) + + def __call__(self, command): + """ + Call a CLI command (e.g. "show running-config") + + Output text is automatically redirected, decoded and returned. + Multiple commands may be passed as list. + """ + proc = self._call_cmd(command, stdout=subprocess.PIPE) + stdout, stderr = proc.communicate() + if proc.wait() != 0: + raise VtyshException('vtysh returned status %d for command "%s"' + % (proc.returncode, command)) + return stdout.decode('UTF-8') + + def is_config_available(self): + """ + Return False if no frr daemon is running or some other vtysh session is + in 'configuration terminal' mode which will prevent us from making any + configuration changes. + """ + + output = self('configure') + + if 'VTY configuration is locked by other VTY' in output: + print(output) + log.error("vtysh 'configure' returned\n%s\n" % (output)) + return False + + return True + + def exec_file(self, filename): + child = self._call(['-f', filename]) + if child.wait() != 0: + raise VtyshException('vtysh (exec file) exited with status %d' + % (child.returncode)) + + def mark_file(self, filename, stdin=None): + kwargs = {} + if stdin is not None: + kwargs['stdin'] = stdin + + child = self._call(['-m', '-f', filename], + stdout=subprocess.PIPE, **kwargs) + try: + stdout, stderr = child.communicate() + except subprocess.TimeoutExpired: + child.kill() + stdout, stderr = proc.communicate() + raise VtyshException('vtysh call timed out!') + + if child.wait() != 0: + raise VtyshException('vtysh (mark file) exited with status %d:\n%s' + % (child.returncode, stderr)) + + return stdout.decode('UTF-8') + + def mark_show_run(self, daemon = None): + cmd = 'show running-config no-header' + if daemon: + cmd += ' %s' % daemon + show_run = self._call_cmd(cmd, stdout=subprocess.PIPE) + mark = self._call(['-m', '-f', '-'], stdin=show_run.stdout, stdout=subprocess.PIPE) + + show_run.wait() + stdout, stderr = mark.communicate() + mark.wait() + + if show_run.returncode != 0: + raise VtyshException('vtysh (show running-config) exited with status %d:' + % (show_run.returncode)) + if mark.returncode != 0: + raise VtyshException('vtysh (mark running-config) exited with status %d' + % (mark.returncode)) + + return stdout.decode('UTF-8') class Context(object): @@ -110,11 +212,12 @@ class Config(object): ('router ospf' for example) are our dictionary key. """ - def __init__(self): + def __init__(self, vtysh): self.lines = [] self.contexts = OrderedDict() + self.vtysh = vtysh - def load_from_file(self, filename, bindir, confdir): + def load_from_file(self, filename): """ Read configuration from specified file and slurp it into internal memory The internal representation has been marked appropriately by passing it @@ -122,15 +225,9 @@ class Config(object): """ log.info('Loading Config object from file %s', filename) - try: - file_output = subprocess.check_output([str(bindir + '/vtysh'), '-m', '-f', filename, '--config_dir', confdir], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - ve = VtyshMarkException(e) - ve.output = e.output - raise ve + file_output = self.vtysh.mark_file(filename) - for line in file_output.decode('utf-8').split('\n'): + for line in file_output.split('\n'): line = line.strip() # Compress duplicate whitespaces @@ -144,7 +241,7 @@ class Config(object): self.load_contexts() - def load_from_show_running(self, bindir, confdir, daemon): + def load_from_show_running(self, daemon): """ Read running configuration and slurp it into internal memory The internal representation has been marked appropriately by passing it @@ -152,16 +249,9 @@ class Config(object): """ log.info('Loading Config object from vtysh show running') - try: - config_text = subprocess.check_output( - bindir + "/vtysh --config_dir " + confdir + " -c 'show run " + daemon + "' | /usr/bin/tail -n +4 | " + bindir + "/vtysh --config_dir " + confdir + " -m -f -", - shell=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - ve = VtyshMarkException(e) - ve.output = e.output - raise ve + config_text = self.vtysh.mark_show_run(daemon) - for line in config_text.decode('utf-8').split('\n'): + for line in config_text.split('\n'): line = line.strip() if (line == 'Building configuration...' or @@ -547,60 +637,7 @@ end self.save_contexts(ctx_keys, current_context_lines) -def line_to_vtysh_conft(ctx_keys, line, delete, bindir, confdir): - """ - Return the vtysh command for the specified context line - """ - - cmd = [] - cmd.append(str(bindir + '/vtysh')) - cmd.append('--config_dir') - cmd.append(confdir) - cmd.append('-c') - cmd.append('conf t') - - if line: - for ctx_key in ctx_keys: - cmd.append('-c') - cmd.append(ctx_key) - - line = line.lstrip() - - if delete: - cmd.append('-c') - - if line.startswith('no '): - cmd.append('%s' % line[3:]) - else: - cmd.append('no %s' % line) - - else: - cmd.append('-c') - cmd.append(line) - - # If line is None then we are typically deleting an entire - # context ('no router ospf' for example) - else: - - if delete: - - # Only put the 'no' on the last sub-context - for ctx_key in ctx_keys: - cmd.append('-c') - - if ctx_key == ctx_keys[-1]: - cmd.append('no %s' % ctx_key) - else: - cmd.append('%s' % ctx_key) - else: - for ctx_key in ctx_keys: - cmd.append('-c') - cmd.append(ctx_key) - - return cmd - - -def line_for_vtysh_file(ctx_keys, line, delete): +def lines_to_config(ctx_keys, line, delete): """ Return the command as it would appear in frr.conf """ @@ -613,6 +650,10 @@ def line_for_vtysh_file(ctx_keys, line, delete): line = line.lstrip() indent = len(ctx_keys) * ' ' + # There are some commands that are on by default so their "no" form will be + # displayed in the config. "no bgp default ipv4-unicast" is one of these. + # If we need to remove this line we do so by adding "bgp default ipv4-unicast", + # not by doing a "no no bgp default ipv4-unicast" if delete: if line.startswith('no '): cmd.append('%s%s' % (indent, line[3:])) @@ -625,26 +666,17 @@ def line_for_vtysh_file(ctx_keys, line, delete): # If line is None then we are typically deleting an entire # context ('no router ospf' for example) else: + for i, ctx_key in enumerate(ctx_keys[:-1]): + cmd.append('%s%s' % (' ' * i, ctx_key)) + + # Only put the 'no' on the last sub-context if delete: - - # Only put the 'no' on the last sub-context - for ctx_key in ctx_keys: - - if ctx_key == ctx_keys[-1]: - cmd.append('no %s' % ctx_key) - else: - cmd.append('%s' % ctx_key) + if ctx_keys[-1].startswith('no '): + cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1][3:])) + else: + cmd.append('%sno %s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1])) else: - for ctx_key in ctx_keys: - cmd.append(ctx_key) - - cmd = '\n' + '\n'.join(cmd) - - # There are some commands that are on by default so their "no" form will be - # displayed in the config. "no bgp default ipv4-unicast" is one of these. - # If we need to remove this line we do so by adding "bgp default ipv4-unicast", - # not by doing a "no no bgp default ipv4-unicast" - cmd = cmd.replace('no no ', '') + cmd.append('%s%s' % (' ' * (len(ctx_keys) - 1), ctx_keys[-1])) return cmd @@ -999,6 +1031,7 @@ def ignore_unconfigurable_lines(lines_to_add, lines_to_del): if (ctx_keys[0].startswith('frr version') or ctx_keys[0].startswith('frr defaults') or + ctx_keys[0].startswith('username') or ctx_keys[0].startswith('password') or ctx_keys[0].startswith('line vty') or @@ -1007,7 +1040,7 @@ def ignore_unconfigurable_lines(lines_to_add, lines_to_del): # by removing this. ctx_keys[0].startswith('service integrated-vtysh-config')): - log.info("(%s, %s) cannot be removed" % (pformat(ctx_keys), line)) + log.info('"%s" cannot be removed' % (ctx_keys[-1],)) lines_to_del_to_del.append((ctx_keys, line)) for (ctx_keys, line) in lines_to_del_to_del: @@ -1126,32 +1159,6 @@ def compare_context_objects(newconf, running): return (lines_to_add, lines_to_del) - -def vtysh_config_available(bindir, confdir): - """ - Return False if no frr daemon is running or some other vtysh session is - in 'configuration terminal' mode which will prevent us from making any - configuration changes. - """ - - try: - cmd = [str(bindir + '/vtysh'), '--config_dir', confdir, '-c', 'conf t'] - output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip() - - if 'VTY configuration is locked by other VTY' in output.decode('utf-8'): - print(output) - log.error("'%s' returned\n%s\n" % (' '.join(cmd), output)) - return False - - except subprocess.CalledProcessError as e: - msg = "vtysh could not connect with any frr daemons" - print(msg) - log.error(msg) - return False - - return True - - if __name__ == '__main__': # Command line options parser = argparse.ArgumentParser(description='Dynamically apply diff in frr configs') @@ -1166,6 +1173,7 @@ if __name__ == '__main__': parser.add_argument('--bindir', help='path to the vtysh executable', default='/usr/bin') parser.add_argument('--confdir', help='path to the daemon config files', default='/etc/frr') parser.add_argument('--rundir', help='path for the temp config file', default='/var/run/frr') + parser.add_argument('--vty_socket', help='socket to be used by vtysh to connect to the daemons', default=None) parser.add_argument('--daemon', help='daemon for which want to replace the config', default='') args = parser.parse_args() @@ -1221,6 +1229,13 @@ if __name__ == '__main__': log.error(msg) sys.exit(1) + # verify that the vty_socket, if specified, is valid + if args.vty_socket and not os.path.isdir(args.vty_socket): + msg = 'vty_socket %s is not a valid path' % args.vty_socket + print(msg) + log.error(msg) + sys.exit(1) + # verify that the daemon, if specified, is valid if args.daemon and args.daemon not in ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd']: msg = "Daemon %s is not a valid option for 'show running-config'" % args.daemon @@ -1228,6 +1243,8 @@ if __name__ == '__main__': log.error(msg) sys.exit(1) + vtysh = Vtysh(args.bindir, args.confdir, args.vty_socket) + # Verify that 'service integrated-vtysh-config' is configured vtysh_filename = args.confdir + '/vtysh.conf' service_integrated_vtysh_config = True @@ -1253,19 +1270,19 @@ if __name__ == '__main__': log.info('Called via "%s"', str(args)) # Create a Config object from the config generated by newconf - newconf = Config() - newconf.load_from_file(args.filename, args.bindir, args.confdir) + newconf = Config(vtysh) + newconf.load_from_file(args.filename) reload_ok = True if args.test: # Create a Config object from the running config - running = Config() + running = Config(vtysh) if args.input: - running.load_from_file(args.input, args.bindir, args.confdir) + running.load_from_file(args.input) else: - running.load_from_show_running(args.bindir, args.confdir, args.daemon) + running.load_from_show_running(args.daemon) (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) lines_to_configure = [] @@ -1279,7 +1296,7 @@ if __name__ == '__main__': if line == '!': continue - cmd = line_for_vtysh_file(ctx_keys, line, True) + cmd = '\n'.join(lines_to_config(ctx_keys, line, True)) lines_to_configure.append(cmd) print(cmd) @@ -1292,14 +1309,14 @@ if __name__ == '__main__': if line == '!': continue - cmd = line_for_vtysh_file(ctx_keys, line, False) + cmd = '\n'.join(lines_to_config(ctx_keys, line, False)) lines_to_configure.append(cmd) print(cmd) elif args.reload: # We will not be able to do anything, go ahead and exit(1) - if not vtysh_config_available(args.bindir, args.confdir): + if not vtysh.is_config_available(): sys.exit(1) log.debug('New Frr Config\n%s', newconf.get_lines()) @@ -1342,8 +1359,8 @@ if __name__ == '__main__': lines_to_add_first_pass = [] for x in range(2): - running = Config() - running.load_from_show_running(args.bindir, args.confdir, args.daemon) + running = Config(vtysh) + running.load_from_show_running(args.daemon) log.debug('Running Frr Config (Pass #%d)\n%s', x, running.get_lines()) (lines_to_add, lines_to_del) = compare_context_objects(newconf, running) @@ -1375,7 +1392,7 @@ if __name__ == '__main__': # 'no' commands are tricky, we can't just put them in a file and # vtysh -f that file. See the next comment for an explanation # of their quirks - cmd = line_to_vtysh_conft(ctx_keys, line, True, args.bindir, args.confdir) + cmd = lines_to_config(ctx_keys, line, True) original_cmd = cmd # Some commands in frr are picky about taking a "no" of the entire line. @@ -1394,9 +1411,9 @@ if __name__ == '__main__': while True: try: - _ = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + vtysh(['configure'] + cmd) - except subprocess.CalledProcessError: + except VtyshException: # - Pull the last entry from cmd (this would be # 'no ip ospf authentication message-digest 1.1.1.1' in @@ -1406,7 +1423,7 @@ if __name__ == '__main__': last_arg = cmd[-1].split(' ') if len(last_arg) <= 2: - log.error('"%s" we failed to remove this command', original_cmd) + log.error('"%s" we failed to remove this command', ' -- '.join(original_cmd)) break new_last_arg = last_arg[0:-1] @@ -1428,7 +1445,7 @@ if __name__ == '__main__': if x == 1 and ctx_keys[0].startswith('no '): continue - cmd = line_for_vtysh_file(ctx_keys, line, False) + cmd = '\n'.join(lines_to_config(ctx_keys, line, False)) + '\n' lines_to_configure.append(cmd) if lines_to_configure: @@ -1444,16 +1461,16 @@ if __name__ == '__main__': fh.write(line + '\n') try: - subprocess.check_output([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-f', filename], stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - log.warning("frr-reload.py failed due to\n%s" % e.output) + vtysh.exec_file(filename) + except VtyshException as e: + log.warning("frr-reload.py failed due to\n%s" % e.args) reload_ok = False os.unlink(filename) # Make these changes persistent target = str(args.confdir + '/frr.conf') if args.overwrite or (not args.daemon and args.filename != target): - subprocess.call([str(args.bindir + '/vtysh'), '--config_dir', args.confdir, '-c', 'write']) + vtysh('write') if not reload_ok: sys.exit(1) diff --git a/vtysh/vtysh.c b/vtysh/vtysh.c index 21280ec7aa..29e0842daf 100644 --- a/vtysh/vtysh.c +++ b/vtysh/vtysh.c @@ -707,7 +707,7 @@ int vtysh_mark_file(const char *filename) } vty = vty_new(); - vty->wfd = STDERR_FILENO; + vty->wfd = STDOUT_FILENO; vty->type = VTY_TERM; vty->node = CONFIG_NODE; @@ -2845,17 +2845,22 @@ DEFUNSH(VTYSH_ALL, no_vtysh_config_enable_password, DEFUN (vtysh_write_terminal, vtysh_write_terminal_cmd, - "write terminal ["DAEMONS_LIST"]", + "write terminal ["DAEMONS_LIST"] [no-header]", "Write running configuration to memory, network, or terminal\n" "Write to terminal\n" - DAEMONS_STR) + DAEMONS_STR + "Skip \"Building configuration...\" header\n") { unsigned int i; char line[] = "do write terminal\n"; - vty_out(vty, "Building configuration...\n"); - vty_out(vty, "\nCurrent configuration:\n"); - vty_out(vty, "!\n"); + if (!strcmp(argv[argc - 1]->arg, "no-header")) + argc--; + else { + vty_out(vty, "Building configuration...\n"); + vty_out(vty, "\nCurrent configuration:\n"); + vty_out(vty, "!\n"); + } for (i = 0; i < array_size(vtysh_client); i++) if ((argc < 3) @@ -2874,10 +2879,11 @@ DEFUN (vtysh_write_terminal, DEFUN (vtysh_show_running_config, vtysh_show_running_config_cmd, - "show running-config ["DAEMONS_LIST"]", + "show running-config ["DAEMONS_LIST"] [no-header]", SHOW_STR "Current operating configuration\n" - DAEMONS_STR) + DAEMONS_STR + "Skip \"Building configuration...\" header\n") { return vtysh_write_terminal(self, vty, argc, argv); }