Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools: Accelerate config deletion in frr-reload.py by bulk execution #14927

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 143 additions & 91 deletions tools/frr-reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def iteritems(d):


class VtyshException(Exception):
pass
def __init__(self, *args, **kwargs):
super(VtyshException, self).__init__(*args)
self.stderr = kwargs.get("stderr", None)


class Vtysh(object):
Expand Down Expand Up @@ -101,10 +103,12 @@ def is_config_available(self):
return True

def exec_file(self, filename):
child = self._call(["-f", filename])
child = self._call(["-f", filename], stderr=subprocess.PIPE)
_, stderr = child.communicate()
if child.wait() != 0:
raise VtyshException(
"vtysh (exec file) exited with status %d" % (child.returncode)
"vtysh (exec file) exited with status %d" % (child.returncode),
stderr=stderr,
)

def mark_file(self, filename, stdin=None):
Expand Down Expand Up @@ -1835,6 +1839,140 @@ def compare_context_objects(newconf, running):
return (lines_to_add, lines_to_del)


def get_failed_cmds(stderr, filename):
"""
Extract the line numbers of failed lines from the given stderr output
and retrieve the corresponding commands from the file.
"""
failed_line_nums = []
for line in stderr.split("\n"):
# Example stderr:
# line 7: % Unknown command[4]: ...
# line 11: % Unknown command[4]: ...
if line.startswith("line") and "Unknown command" in line:
failed_line_num = line.split()[1][:-1]
if failed_line_num.isdigit():
failed_line_nums.append(int(failed_line_num))

with open(filename) as fh:
lines = ["!"] + [line.rstrip("\n") for line in fh.readlines()]

# In the file, each command is separated by a '!' line.
# If a command is not a single-line command, it will be followed by
# one or more 'exit' lines. We need to find the start and end of
# each command that contains a failed line so that we can extract
# the failed commands from the file.
failed_cmds = OrderedDict()
for num in failed_line_nums:
if lines[num].strip() in {"!", "exit"}:
continue

cmd_start = cmd_end = num
while cmd_start > 1 and lines[cmd_start - 1] != "!":
cmd_start -= 1
while cmd_end < len(lines) and lines[cmd_end].strip() not in {"!", "exit"}:
cmd_end += 1

failed_cmds[cmd_start] = lines[cmd_start:cmd_end]

return list(failed_cmds.values())


def exec_lines(lines, delete, x):
exec_suceess = True
lines_to_configure = []
failed_cmds = []

for ctx_keys, line in lines:
if line == "!":
continue

# Don't run "no" commands twice since they can error
# out the second time due to first deletion
if not delete and x == 1 and ctx_keys[0].startswith("no "):
continue

cmd = "\n".join(lines_to_config(ctx_keys, line, delete)) + "\n"
lines_to_configure.append(cmd)

if lines_to_configure:
random_string = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(6)
)

filename = args.rundir + "/reload-%s.txt" % random_string
log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))

with open(filename, "w") as fh:
for line in lines_to_configure:
fh.write(line + "!\n")

try:
vtysh.exec_file(filename)
except VtyshException as e:
if delete:
log.info("Failed to execute deletion script due to\n%s" % e.args)
failed_cmds = get_failed_cmds(e.stderr, filename)
else:
log.warning("Failed to execute addition script due to\n%s" % e.args)
exec_suceess = False

os.unlink(filename)

for cmd in failed_cmds:
# '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
original_cmd = cmd

# Some commands in frr are picky about taking a "no" of the entire line.
# OSPF is bad about this, you can't "no" the entire line, you have to "no"
# only the beginning. If we hit one of these command an exception will be
# thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
#
# Example:
# frr(config-if)# ip ospf authentication message-digest 1.1.1.1
# frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
# % Unknown command.
# frr(config-if)# no ip ospf authentication message-digest
# % Unknown command.
# frr(config-if)# no ip ospf authentication
# frr(config-if)#
stdouts = []
while True:
try:
vtysh(["configure"] + cmd, stdouts)

except VtyshException:
# - Pull the last entry from cmd (this would be
# 'no ip ospf authentication message-digest 1.1.1.1' in
# our example above
# - Split that last entry by whitespace and drop the last word
log.info("Failed to execute %s", " ".join(cmd))
last_arg = cmd[-1].split(" ")

if len(last_arg) <= 2:
log.error(
'"%s" we failed to remove this command',
" -- ".join(original_cmd),
)
# Log first error msg for original_cmd
if stdouts:
log.error(stdouts[0])
exec_suceess = False
break

new_last_arg = last_arg[0:-1]
cmd[-1] = " ".join(new_last_arg)

else:
log.info('Executed "%s"', " ".join(cmd))
break

return exec_suceess


if __name__ == "__main__":
# Command line options
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -2166,96 +2304,10 @@ def compare_context_objects(newconf, running):
# apply to other scenarios as well where configuring FOO adds BAR
# to the config.
if lines_to_del and x == 0:
for ctx_keys, line in lines_to_del:
if line == "!":
continue

# '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 = lines_to_config(ctx_keys, line, True)
original_cmd = cmd

# Some commands in frr are picky about taking a "no" of the entire line.
# OSPF is bad about this, you can't "no" the entire line, you have to "no"
# only the beginning. If we hit one of these command an exception will be
# thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
#
# Example:
# frr(config-if)# ip ospf authentication message-digest 1.1.1.1
# frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
# % Unknown command.
# frr(config-if)# no ip ospf authentication message-digest
# % Unknown command.
# frr(config-if)# no ip ospf authentication
# frr(config-if)#

stdouts = []
while True:
try:
vtysh(["configure"] + cmd, stdouts)

except VtyshException:
# - Pull the last entry from cmd (this would be
# 'no ip ospf authentication message-digest 1.1.1.1' in
# our example above
# - Split that last entry by whitespace and drop the last word
log.error("Failed to execute %s", " ".join(cmd))
last_arg = cmd[-1].split(" ")

if len(last_arg) <= 2:
log.error(
'"%s" we failed to remove this command',
" -- ".join(original_cmd),
)
# Log first error msg for original_cmd
if stdouts:
log.error(stdouts[0])
reload_ok = False
break

new_last_arg = last_arg[0:-1]
cmd[-1] = " ".join(new_last_arg)
else:
log.info('Executed "%s"', " ".join(cmd))
break
reload_ok = exec_lines(lines_to_del, True, x) and reload_ok

if lines_to_add:
lines_to_configure = []

for ctx_keys, line in lines_to_add:
if line == "!":
continue

# Don't run "no" commands twice since they can error
# out the second time due to first deletion
if x == 1 and ctx_keys[0].startswith("no "):
continue

cmd = "\n".join(lines_to_config(ctx_keys, line, False)) + "\n"
lines_to_configure.append(cmd)

if lines_to_configure:
random_string = "".join(
random.SystemRandom().choice(
string.ascii_uppercase + string.digits
)
for _ in range(6)
)

filename = args.rundir + "/reload-%s.txt" % random_string
log.info("%s content\n%s" % (filename, pformat(lines_to_configure)))

with open(filename, "w") as fh:
for line in lines_to_configure:
fh.write(line + "\n")

try:
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)
reload_ok = exec_lines(lines_to_add, False, x) and reload_ok

# Make these changes persistent
target = str(args.confdir + "/frr.conf")
Expand Down
Loading