{"id":16216822,"url":"https://github.com/adamcharnock/routeros-diff","last_synced_at":"2025-03-19T10:30:41.525Z","repository":{"id":57462749,"uuid":"345214128","full_name":"adamcharnock/routeros-diff","owner":"adamcharnock","description":"Diff and prettify RouterOS configuration files. Create configuration patches for your Mikrotik routers!","archived":false,"fork":false,"pushed_at":"2021-06-28T09:12:41.000Z","size":242,"stargazers_count":13,"open_issues_count":1,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-14T14:48:06.537Z","etag":null,"topics":["mikrotik","networking","routeros"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adamcharnock.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-03-06T23:05:35.000Z","updated_at":"2024-04-11T00:02:47.000Z","dependencies_parsed_at":"2022-09-05T17:21:53.556Z","dependency_job_id":null,"html_url":"https://github.com/adamcharnock/routeros-diff","commit_stats":null,"previous_names":["adamcharnock/routeros-diff","gardunha/routeros-diff"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamcharnock%2Frouteros-diff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamcharnock%2Frouteros-diff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamcharnock%2Frouteros-diff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamcharnock%2Frouteros-diff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamcharnock","download_url":"https://codeload.github.com/adamcharnock/routeros-diff/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244407719,"owners_count":20447841,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mikrotik","networking","routeros"],"created_at":"2024-10-10T11:22:42.988Z","updated_at":"2025-03-19T10:30:41.275Z","avatar_url":"https://github.com/adamcharnock.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Create configuration patches for your Mikrotik routers\n\n[![PyPI license](https://img.shields.io/pypi/l/routeros-diff.svg)](https://pypi.python.org/pypi/routeros-diff/)\n[![PyPI pyversions](https://img.shields.io/pypi/pyversions/routeros-diff.svg)](https://pypi.python.org/pypi/routeros-diff/)\n[![Tests](https://github.com/gardunha/routeros-diff/actions/workflows/ci.yaml/badge.svg)](https://github.com/gardunha/routeros-diff/actions/workflows/ci.yaml)\n\n## Installation\n\nInstall using your favourite Python package manager. For example:\n\n    pip install routeros-diff\n\n## Get a diff\n\nThe `routeros_diff` (alias `ros_diff`) command will take two RouterOS files and diff them:\n\n    routeros_diff old_config.rsc new_config.rsc\n\nOr using Python:\n\n```python\nfrom routeros_diff.parser import RouterOSConfig\nnew = RouterOSConfig.parse(new_config_string)\nold = RouterOSConfig.parse(old_config_string)\nprint(new.diff(old))\n```\n\n## Examples:\n\nA simple example first:\n\n```r\n# Old:\n/routing ospf instance\nadd name=core router-id=100.127.0.1\n\n# New:\n/routing ospf instance\nadd name=core router-id=100.127.0.99\n\n# Diff:\n/routing ospf instance\nset [ find name=core ] router-id=100.127.0.99\n```\n\nHere is a more complex example where we use custom IDs in order to maintain \nexpression ordering (see 'Natural Keys \u0026 IDs' below for details):\n\n```r\n# Old:\n/ip firewall nat \nadd chain=a comment=\"Example text [ ID:block-smtp ]\"\nadd chain=c comment=\"[ ID:block-smb ]\"\n\n# New:\n/ip firewall nat \nadd chain=a comment=\"Example text [ ID:block-smtp ]\"\nadd chain=b comment=\"[ ID:block-nfs ]\"\nadd chain=c comment=\"[ ID:block-smb ]\"\n\n# Diff:\n/ip firewall nat \nadd chain=b comment=\"[ ID:block-nfs ]\" place-before=[ find where comment~ID:block-smb ]\n```\n\n### Usage \u0026 limitations\n\nThis aim is for this diffing process to work well within a limited range of conditions.\nThe configuration format is an entire scripting language in itself, and so this library\ncannot sensibly hope to parse any arbitrary input. As a rule of thumb, this library should\nbe able to diff anything produced by `/export`.\n\n### Advanced use\n\n`RouterOSConfig.parse` also accepts a second optional parameter as follows:\n\n```python\nfrom routeros_diff.parser import RouterOSConfig\nnew = RouterOSConfig.parse(new_config_string)\nold = RouterOSConfig.parse(old_config_string)\n\n# Produced using: /export verbose\nold_verbose = RouterOSConfig.parse(old_verbose_config_string)\n\nprint(new.diff(old, old_verbose))\n```\n\nProviding `old_verbose` allows the diffing algorithm to be a smarter in the \ndiff it produces. When `old_verbose` is provided, the algorithm can automatically \navoid setting certain values which it knows to be unchanged. This only \napplies in cases where both a) the new config sets an argument back to its \ndefault value, and b) the old config already has the argument set the equal value.\n\nWhile this feature isn't required to produce functioning diffs, it does \nmake it easier to produce diffs without unnecessary expressions. To put it another way,\nuse this method if you want to be sure that diffing two functionally-equal configurations \nproduces an empty diff.\n\n### Sections and expressions\n\nThe following is NOT supported:\n\n```r\n## NOT SUPPORTED, DONT DO THIS ##\n/routing ospf instance add name=core router-id=100.127.0.1\n```\n\nRather, this must be formatted as separate 'sections' and 'expressions' on different lines. For example:\n\n```r\n/routing ospf instance \nadd name=core router-id=100.127.0.1\n```\n\nThe section in this example is `/routing ospf instance`, and the expression is `add name=core router-id=100.127.0.1`.\nEach section may contain multiple expressions (just like the output you see from `/export`).\n\n### Natural Keys \u0026 IDs\n\nThe parser will try to uniquely identify each expression. This allows the parser to be intelligent regarding\nadditions, modifications, deletions, and ordering.\n\nThe parser refers to these unique identities as naturals keys \u0026 natural IDs. For example:\n\n```r\nadd name=core router-id=100.127.0.1\n```\n\nHere the natural key is `name` and the natural ID is `core`. The parser assumes `name` will be the natural key,\nbut is configured to use other keys in some situations.\n\nAdditionally, you can choose to manually add your own IDs to expressions. This is done using comments.\nFor example:\n\n```r\nadd chain=a comment=\"[ ID:1 ]\"\n```\n\nThese comment-based IDs take priority over whatever the parser may have otherwise used.\nIf using comment IDs, you should make sure you set them for all expressions in\nthat section.\n\nThis is especially useful for firewall rules. The order of firewall rules is important, and they have no\nobvious natural keys/IDs. Using comments IDs for your firewall rules allows the parser to\nintelligently maintain order. For example:\n\n```r\n# Old:\n/ip firewall nat \nadd chain=a comment=\"Example text [ ID:block-smtp ]\"\nadd chain=c comment=\"[ ID:block-smb ]\"\n\n# New:\n/ip firewall nat \nadd chain=a comment=\"Example text [ ID:block-smtp ]\"\nadd chain=b comment=\"[ ID:block-nfs ]\"\nadd chain=c comment=\"[ ID:block-smb ]\"\n\n# Diff:\n/ip firewall nat \nadd chain=b comment=\"[ ID:block-nfs ]\" place-before=[ find where comment~ID:block-smb ]\n```\n\nNote that the parser uses `place-before` to correctly place the new firewall rule.\n\n*Without using comment IDs, the parse would have to drop and recreate all firewall rules.* This would\nbe non-ideal for reasons of both security and reliability.\n\n### Reporting errors\n\nSeeing something strange in your diff output? Please report the error with the following information:\n\n* The input\n* The actual output\n* What you think the output should be instead\n\nPlease minimise the size of this data as much as possible. The smaller and more specific the example of the problem,\nthe easier it will be for us to find a resolution.\n\n## Prettify\n\nThe `routeros_prettify` (alias `ros_prettify`) command will parse an existing configuration and re-print it in a\nstandard format with common sections collapsed:\n\n```r\nrouteros_prettify old_config.rsc new_config.rsc\n```\n\nOr using Python:\n\n```python\nfrom routeros_diff.parser import RouterOSConfig\nconfig = RouterOSConfig.parse(config_string)\nprint(config)\n```\n\nYou can also produce a syntax-highlighted HTML version of the configuration as follows\n([see example css](https://github.com/gardunha/routeros-diff/blob/main/extra/ros-syntax.css)):\n\n```python\nfrom routeros_diff.parser import RouterOSConfig\nconfig = RouterOSConfig.parse(config_string)\nprint(config.__html__())\n```\n\n## Settings\n\nYou can customise settings in one of two ways.\n\nThe simplest way is to pass settings to RouterOSConfig.parse():\n\n```python\nRouterOSConfig.parse(s=my_config, settings=dict(\n    # Natural keys for each section name.\n    # 'name' will be used if none is found below\n    # (and only if the 'name' value is available)\n    natural_keys={\n        \"/ip address\": \"address\", \n        ...\n    },\n    \n    # Don't perform deletions in these sections\n    no_deletions={\n        \"/interface ethernet\", \n        ...\n    },\n    \n    # Don't perform creations in these sections\n    no_creations={\n        \"/interface ethernet\",\n        ...\n    },\n    \n    # Ordering is important in these sections. Ensure \n    # entities maintain their order. Natural keys/ids must be \n    # present in sections listed here\n    expression_order_important={\n        \"/ip firewall*\", \n        ...\n    },\n))\n```\n\nNote that section paths can be specified using '*' wildcards.\nFor example, `/ip firewall*`.\n\nAlternatively, you can extend this class and override its methods. \nThis allows you to implement more complex logic should you require.\nIn this case, you can pass your customised class to the parser as follows:\n\n    RouterOSConfig.parse(my_config, settings=MyCustomSettings())\n\n## Concepts\n\nThis is a **section** with a path of `/ip address` and two expressions:\n\n```r\n/ip address\nadd address=1.2.3.4\nadd address=5.6.7.8\n```\n\nThis is an **expression** with a command of **add**, and a key-value argument of `address=1.2.3.4`:\n\n```r\nadd address=1.2.3.4\n```\n\n## Release process:\n\n```bash\nexport VERSION=a.b.c\n\npoetry version $VERSION\ndephell convert\nblack setup.py\n\ngit add .\ngit commit -m \"Releasing version $VERSION\"\n\ngit tag \"v$VERSION\"\ngit branch \"v$VERSION\"\ngit push origin \\\n    refs/tags/\"v$VERSION\" \\\n    refs/heads/\"v$VERSION\" \\\n    main\n\n# Wait for CI to pass\n\npoetry publish --build\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamcharnock%2Frouteros-diff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamcharnock%2Frouteros-diff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamcharnock%2Frouteros-diff/lists"}