Comparing config files

Normally I have my configs stored on GIT with comments etc. Normally my workflow is to make some on-the-spot changes via winbox, then when everything is working I populate the changes into the GIT version. The issue is life isn’t perfect: it happened to me many times I or someone else forgot to populate the change in the GIT copy… which is less than ideal.
Later on I discover a stray firewall rule and I wonder what else is missing :slight_smile:

The only idea of comparing configs I had is exporting a file from the device and then emulating it on CHR, loading GIT version and exporting the config. Then it becomes a simple matter of diffing both configs. However, this isn’t practical as e.g. QEmu on my macOS cannot emulate >10 interfaces nor can emulate sfpplus1 port…

Is there any way to compare two different config files? How people do it normally?

Why not automate the export->git part? For example, I have list of routers, script connects to each one using ssh, runs /export, saves output to file (after stripping some comments to avoid unnecessary changes), and finally commits everything. It’s scheduled to run every day, or for some routers even more often. The most I can lose is few hours of changes. And I have full history, so I know exactly when something changed. It’s not perfect. For start, it doesn’t tell who changed it (if more people have access). Even more annoying is that export doesn’t export everything (certificates, etc). But otherwise it’s pretty good.

That was the initial idea but the automatic export is a vomit which usually doesn’t even import (e.g. it has no delays to wait for interfaces). It’s not really editable as sections nor options aren’t organized in a way to make manual editing easy. In addition I tend to e.g. indent chains in the firewall like below. My configs are usually more akin to how you organize code :wink:
Implementing automatic daily export isn’t a bad idea as a backup thou.

I guess I would need some tool to normalize the config and then compare but I’m not sure if it’s possible without execution.
Screen Shot 2022-10-06 at 22.10.17.png

I don’t get why do you need to import the config in order to compare it with another config. What am I missing?
Nevermind, looks like you’re not keeping versions of the actual config in git, but only config changes written in your own personal style.

I use this: https://sourceforge.net/projects/winmerge/ to visualy compare .rsc files

@kiler129: That’s it, I’m mainly after readable backup, I don’t need it to be pretty or even importable as whole, so it works for me.

Coincidence, I was about to open a new thread on a very similar topic.

Lately I needed to compare two configs and spot differences using Meld (winmerge-like tool).
Current and default export behaviour, IMHO, should be changed to get a more deterministic output, maybe using some sorting or filtering.

I thought of post-processing those files myself but these file content don’t look easy to parse.

You use windows?

My backup tool produces diffs as part of its regular behavior. The docs explain why you’re better off not using git for this, but if you must, it wouldn’t be difficult to convert it.

For have more readability, and do not have useless warning,
better replace something inside the export…

“_” => " "

literally \ and \r\n + 4 spaces => “”

“# " followed by date and time and " by RouterOS” => “# RouterOS”

I’ve tried similar approach with handcrafted config files and comparison, and crafted a Python script to compare actual config to one I’d want:

# RouterOS config file parser/sorter/comparer ver.0
# by 611

import sys
import re

# My preferred order of parameter sorting
# Firtsparams will go first in the listed order, then unlisted params in alphabetical order, then lastparams in the listed order
firstparams =	"name", "peer", "list", "time-zone-name", \
		"chain", "action", "address-list", "address-list-timeout", "connection-state", "protocol", \
		"src-address", "src-address-list", "src-port", "dst-address", "dst-address-list", "dst-port", \
		"local-address", "remote-address", \
		"master-interface", "mode", "ssid", "security-profile", "wireless-protocol", "band", "country", "frequency", "channel-width", \
		"server", "address", "mac-address", "network", "target", \
		"leds", "gateway", "distance", "type", \
		"bridge", "interface", "user", "password", \
		"servers", "authentication-types", "wpa2-pre-shared-key"
lastparams =	"disabled", "log", "log-prefix", "comment", "source"

# Sorting switch
dosort = False
doimport = True
includenoncommands = False

# Sort parameters with first list and last list
# Args: dict params; Returns: list of (arg, val) tuples sortedparams
def SortParams(params):
	sortedparams = []

	# First, add all present params on the "first" list in their sort order.
	for p in firstparams:
		if p in params:
			sortedparams.append((p, params.pop(p)))

	# Second, add all params NOT on the "last" list in alphabetical order.
	for p in sorted(params):
		if p not in lastparams:
			sortedparams.append((p, params.pop(p)))

	# Third, add all params on the "last" list in their sort order.
	for p in lastparams:
		if p in params:
			sortedparams.append((p, params.pop(p)))

	# Here we may have checked if there's anything left in params, but it's impossible :)

	# Return sorted list
	return sortedparams


# Make a set out of config list 
# Args: list of tuples (cmd, params); Returns: same as a set 
def MakeSet(config):
	configset = set()
	for line in config:
		(cmd, params) = line
		if (len(params) != 0) | includenoncommands:
			configset.add((cmd, tuple(params)))
	return configset


# Parse a Mikrotik-formatted config line 
# Args: string line; Returns: tuple (cmd, params)
def ParseMTrscLine(line):

	# Split the line into command (starting with "/", with optional condition in "[]")
	# and parameters (starting with "arg=" or "!arg").
	# Suggestions are welcome: this regex that may misbehave on cases like `/cmd [cond="]"] arg=...` -
	# 	in-string closing square bracket would be caught.
	# Command starts with "/", lazy captures anything to the next group, optionally includes "[" lazy capture of condition "]"
	# Parameters are starting with some space and either non-space ending with "=" or non-space starting with "!"
	parsedline = re.match(r"(?P<cmd>/.+?(?:\[.+?\])?)" + r"(?P<params>\s+(?:\S+?(?==)|\!\S+).*)", line)
	# Return None if the line failed to parse (doesn't look like command)
	if parsedline == None:
		return None

	# Split parameters into arument-value pairs (no value for inverted argument starting with  "!"),
	# strip extra spaces between parameters.
	# Suggestions are welcome: this regex that may misbehave on cases like `arg="val\\"` -
	# 	quotes that _look_ escaped are skipped when looking for the end of quoted value,
	#	even though they might be not actually escaped.
	# Parameters are separated (started) with some space,
	# 	and are either (non-space lazy) arg ending with "=" or just a (greedy!) arg starting with "!".
	# Values are optional and starting with with "=", either quoted with any symbols inside (lazy),
	#	or with end-of-line instead of closing quote, or not quoted non-space.
	#	Quotes that _look_ escaped are skipped using negative lookbeihing group.
	# Note that we have to stick to two capruring groups (or do extra processing outside regxp),
	#	so the first group  must catch both "arg=" and "!arg" cases.
	params = re.findall(r"\s*" + r"(?P<arg>\S+?(?==)|!\S+)" + r"(?:=(?P<val>\".*?(?:(?<!\\)\"|\\$)|\S+))?", parsedline.group('params'))

	# Return tuple of command and list of arument-value pairs
	return (parsedline.group('cmd'), params)


# Read a Mikrotik-formatted config line 
# Args: string filename; Returns: list of tuples (cmd, params)
def ReadMTrscFile(filename):

	# Init an empty list
	config = []

	# Open the file and iterate over it
	infile = open(filename, "r")
	for line in infile:

		# Parse the line
		parsedline = ParseMTrscLine(line)

		# If the line failed to parse, just add it to the list as-is with empty params list
		if parsedline == None:
			config.append((line.rstrip("\n"), []))
			continue

		# Extract command and parameters form parsed line
		(cmd, params) = parsedline
		# Chech if we've got an include and if we'll honor it 
		if (cmd == "/import") & doimport: 
		        config += ReadMTrscFile(dict(params)["file"])
		# Add command with sorted parameters if required
		elif dosort:               
			config.append((cmd, SortParams(dict(params))))
		# Add tuple as-is if we don't sort
		else:
			config.append(parsedline)

	# Finished with file
	infile.close()

	# Return the list containing the configuration
	return config


# Write a Mikrotik-formatted config line 
# Args: string filename, list of tuples (cmd, params)
def WriteMTrscFile(filename, config):

	# Open the file and iterate over config
	outfile = open(filename, "w")
	for line in config:

		# Extract parts from tuple and compose new line (staring with command and continuing with space separated argument[=value] blocks)
		(cmd, params) = line
		newline = cmd
		for p in params:
			newline += " " + p[0]
			if p[1] != '':
				newline += "=" + p[1]

		# Parse the line
		outfile.write(newline + "\n")

	# Finished with file
	outfile.close()

	# Return nothing
	return 

	

command = sys.argv[1]

if command == "parse":
	dosort = False
	infilename = sys.argv[2]
	outfilename = sys.argv[3]
	WriteMTrscFile(outfilename, ReadMTrscFile(infilename))

elif command == "sort":
	dosort = True
	infilename = sys.argv[2]
	outfilename = sys.argv[3]
	WriteMTrscFile(outfilename, ReadMTrscFile(infilename))

elif command == "compare":
	dosort = True
	in1filename = sys.argv[2]
	in2filename = sys.argv[3]
	diff12filename = sys.argv[4]
	diff21filename = sys.argv[5]
	config1 = ReadMTrscFile(in1filename)                        
	configset1 = MakeSet(config1)
	config2 = ReadMTrscFile(in2filename)                        
	configset2 = MakeSet(config2)
	configset12 = configset1.difference(configset2)
	configset21 = configset2.difference(configset1)
	WriteMTrscFile(diff12filename, configset12)
	WriteMTrscFile(diff21filename, configset21)

else:
	print("Usage: MTrscTools <parse|sort|compare>")
	exit


#WriteMTrscFile(outfilename, config)

# That's all folks!

The thing is unfinished as I never had a time to do it. Use at your discretion.

Notepad++ and Compare plugin here.

«diff -r -I ‘#by RouterOS’» powershell and windiff

When you want an export that is easier to “diff” and less prone to showing lots of differences for only a single keyword change, use /export terse
Of course in v7 use /export show-sensitive terse

@pe1chl thank you, no one wrote it and I hadn’t even thought about writing the right command to use to perform the export…

:+1::+1::+1:


As @pe1chl wrote:
for v6

# export all except users parts (and no certificates, dude database, etc.)
/export terse file=export.rsc

# export all about users and groups except passwords
/user export terse file=user_export.rsc

for v7

# export all except users parts (and no certificates, dude database, etc.)
/export show-sensitive terse file=export.rsc

# export all about users and groups except passwords
/user export show-sensitive terse file=user_export.rsc

All these years, never knew what “terse” does…

I hate to admit it, but you’re not alone. :laughing:

The difference is that “terse” outputs complete config lines and does not break them at around 80 characters.
So instead of:

/ip firewall mangle
add action=mark-packet chain=priority comment="Priority 0" new-packet-mark=\
    prio0 passthrough=no priority=0
add action=mark-packet chain=priority comment="Priority 1" new-packet-mark=\
    prio1 passthrough=no priority=1
add action=mark-packet chain=priority comment="Priority 2" new-packet-mark=\
    prio2 passthrough=no priority=2
add action=mark-packet chain=priority comment="Priority 3" new-packet-mark=\
    prio3 passthrough=no priority=3
add action=mark-packet chain=priority comment="Priority 4" new-packet-mark=\
    prio4 passthrough=no priority=4
add action=mark-packet chain=priority comment="Priority 5" new-packet-mark=\
    prio5 passthrough=no priority=5
add action=mark-packet chain=priority comment="Priority 6" new-packet-mark=\
    prio6 passthrough=no priority=6
add action=mark-packet chain=priority comment="Priority 7" new-packet-mark=\
    prio7 passthrough=no priority=7

it outputs:

/ip firewall mangle add action=mark-packet chain=priority comment="Priority 0" new-packet-mark=prio0 passthrough=no priority=0
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 1" new-packet-mark=prio1 passthrough=no priority=1
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 2" new-packet-mark=prio2 passthrough=no priority=2
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 3" new-packet-mark=prio3 passthrough=no priority=3
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 4" new-packet-mark=prio4 passthrough=no priority=4
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 5" new-packet-mark=prio5 passthrough=no priority=5
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 6" new-packet-mark=prio6 passthrough=no priority=6
/ip firewall mangle add action=mark-packet chain=priority comment="Priority 7" new-packet-mark=prio7 passthrough=no priority=7

That means that when you insert or delete a line, you always know its section, and when you change some option in the middle it does not change the position of a line break.
This the diffs are much easier to identify, when you use some diff tool that colorizes the parts that changed.

I tried using ‘tense’ but that didnt work…

But yes, thanks but why have you been holding out on us for so long with this gem :slight_smile:

In terms of comparing configs I put two configs side by side in notepadd ++ and use compare command or something similar Works well!!