Package: routeros-diff
Create configuration patches for your Mikrotik routersInstallationInstall using your favourite Python package manager. For example:pip install routeros-diffGet a diffTherouteros_diff(aliasros_diff) command will take two RouterOS files and diff them:routeros_diff old_config.rsc new_config.rscOr using Python:fromrouteros_diff.parserimportRouterOSConfignew=RouterOSConfig.parse(new_config_string)old=RouterOSConfig.parse(old_config_string)print(new.diff(old))Examples:A simple example first:# Old:/routingospfinstanceaddname=corerouter-id=100.127.0.1# New:/routingospfinstanceaddname=corerouter-id=100.127.0.99# Diff:/routingospfinstanceset[findname=core]router-id=100.127.0.99Here is a more complex example where we use custom IDs in order to maintain
expression ordering (see 'Natural Keys & IDs' below for details):# Old:/ipfirewallnataddchain=acomment="Example text [ ID:block-smtp ]"addchain=ccomment="[ ID:block-smb ]"# New:/ipfirewallnataddchain=acomment="Example text [ ID:block-smtp ]"addchain=bcomment="[ ID:block-nfs ]"addchain=ccomment="[ ID:block-smb ]"# Diff:/ipfirewallnataddchain=bcomment="[ ID:block-nfs ]"place-before=[findwherecomment~ID:block-smb]Usage & limitationsThis aim is for this diffing process to work well within a limited range of conditions.
The configuration format is an entire scripting language in itself, and so this library
cannot sensibly hope to parse any arbitrary input. As a rule of thumb, this library should
be able to diff anything produced by/export.Advanced useRouterOSConfig.parsealso accepts a second optional parameter as follows:fromrouteros_diff.parserimportRouterOSConfignew=RouterOSConfig.parse(new_config_string)old=RouterOSConfig.parse(old_config_string)# Produced using: /export verboseold_verbose=RouterOSConfig.parse(old_verbose_config_string)print(new.diff(old,old_verbose))Providingold_verboseallows the diffing algorithm to be a smarter in the
diff it produces. Whenold_verboseis provided, the algorithm can automatically
avoid setting certain values which it knows to be unchanged. This only
applies in cases where both a) the new config sets an argument back to its
default value, and b) the old config already has the argument set the equal value.While this feature isn't required to produce functioning diffs, it does
make it easier to produce diffs without unnecessary expressions. To put it another way,
use this method if you want to be sure that diffing two functionally-equal configurations
produces an empty diff.Sections and expressionsThe following is NOT supported:## NOT SUPPORTED, DONT DO THIS ##/routingospfinstanceaddname=corerouter-id=100.127.0.1Rather, this must be formatted as separate 'sections' and 'expressions' on different lines. For example:/routingospfinstanceaddname=corerouter-id=100.127.0.1The section in this example is/routing ospf instance, and the expression isadd name=core router-id=100.127.0.1.
Each section may contain multiple expressions (just like the output you see from/export).Natural Keys & IDsThe parser will try to uniquely identify each expression. This allows the parser to be intelligent regarding
additions, modifications, deletions, and ordering.The parser refers to these unique identities as naturals keys & natural IDs. For example:addname=corerouter-id=100.127.0.1Here the natural key isnameand the natural ID iscore. The parser assumesnamewill be the natural key,
but is configured to use other keys in some situations.Additionally, you can choose to manually add your own IDs to expressions. This is done using comments.
For example:addchain=acomment="[ ID:1 ]"These comment-based IDs take priority over whatever the parser may have otherwise used.
If using comment IDs, you should make sure you set them for all expressions in
that section.This is especially useful for firewall rules. The order of firewall rules is important, and they have no
obvious natural keys/IDs. Using comments IDs for your firewall rules allows the parser to
intelligently maintain order. For example:# Old:/ipfirewallnataddchain=acomment="Example text [ ID:block-smtp ]"addchain=ccomment="[ ID:block-smb ]"# New:/ipfirewallnataddchain=acomment="Example text [ ID:block-smtp ]"addchain=bcomment="[ ID:block-nfs ]"addchain=ccomment="[ ID:block-smb ]"# Diff:/ipfirewallnataddchain=bcomment="[ ID:block-nfs ]"place-before=[findwherecomment~ID:block-smb]Note that the parser usesplace-beforeto correctly place the new firewall rule.Without using comment IDs, the parse would have to drop and recreate all firewall rules.This would
be non-ideal for reasons of both security and reliability.Reporting errorsSeeing something strange in your diff output? Please report the error with the following information:The inputThe actual outputWhat you think the output should be insteadPlease minimise the size of this data as much as possible. The smaller and more specific the example of the problem,
the easier it will be for us to find a resolution.PrettifyTherouteros_prettify(aliasros_prettify) command will parse an existing configuration and re-print it in a
standard format with common sections collapsed:routeros_prettifyold_config.rscnew_config.rscOr using Python:fromrouteros_diff.parserimportRouterOSConfigconfig=RouterOSConfig.parse(config_string)print(config)You can also produce a syntax-highlighted HTML version of the configuration as follows
(see example css):fromrouteros_diff.parserimportRouterOSConfigconfig=RouterOSConfig.parse(config_string)print(config.__html__())SettingsYou can customise settings in one of two ways.The simplest way is to pass settings to RouterOSConfig.parse():RouterOSConfig.parse(s=my_config,settings=dict(# Natural keys for each section name.# 'name' will be used if none is found below# (and only if the 'name' value is available)natural_keys={"/ip address":"address",...},# Don't perform deletions in these sectionsno_deletions={"/interface ethernet",...},# Don't perform creations in these sectionsno_creations={"/interface ethernet",...},# Ordering is important in these sections. Ensure# entities maintain their order. Natural keys/ids must be# present in sections listed hereexpression_order_important={"/ip firewall*",...},))Note that section paths can be specified using '*' wildcards.
For example,/ip firewall*.Alternatively, you can extend this class and override its methods.
This allows you to implement more complex logic should you require.
In this case, you can pass your customised class to the parser as follows:RouterOSConfig.parse(my_config, settings=MyCustomSettings())ConceptsThis is asectionwith a path of/ip addressand two expressions:/ipaddressaddaddress=1.2.3.4addaddress=5.6.7.8This is anexpressionwith a command ofadd, and a key-value argument ofaddress=1.2.3.4:addaddress=1.2.3.4Release process:exportVERSION=a.b.c

poetryversion$VERSIONdephellconvert
blacksetup.py

gitadd.
gitcommit-m"Releasing version$VERSION"gittag"v$VERSION"gitbranch"v$VERSION"gitpushorigin\refs/tags/"v$VERSION"\refs/heads/"v$VERSION"\main# Wait for CI to passpoetrypublish--build