Merge pull request #6466 from opensourcerouting/frr-reload-cleanup

tools/frr-reload: cleanup + `--vty_socket`
This commit is contained in:
Mark Stapp 2020-05-28 11:06:20 -04:00 committed by GitHub
commit 5e5e7a6cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 150 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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);
}