initial commit

This commit is contained in:
Krystian Dużyński 2022-11-04 21:20:16 +01:00
commit cb6a0f8d27
31 changed files with 880 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/.idea/

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Krystian Dużyński
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

129
README.md Normal file
View file

@ -0,0 +1,129 @@
Mikrotik configurator
======
A utility for generating and applying RouterOS / Mikrotik configuration files (.rsc).
It uses Jinja2 as a template engine with few additional helpers.
Features:
* Applying configuration over SSH,
* Support for applying only part of the whole configuration (e.g. if only firewall settings have changed).
Helpers:
* Escaping string and blocks,
* Embedding files (e.g. SSH keys),
* Cleanup functions (executed in reverse order on configuration re-apply),
* Rolling back firewall chains.
Additional:
* [IntelliJ color schema configuration](intellij) for .rsc files,
* [A script](tools/gen_ssh_host_keys.sh) for generating RouterOS-compatible SSH host key files.
# Usage
Prepare template files, like from [the embedded example](example):
* 0_0-initial.rsc
* 0_1-security.rsc
* 0_2-firewall.rsc
* 1-logging.rsc
* 2-ntp.rsc
* 3-port-forwarding.rsc
Naming is important:
* filenames starting with `0_` are considered reset configuration that is being applied after
`/system reset-configuration` command is executed,
* filenames starting with other numbers can be applied to actively running Mikrotik router,
* adding numbers help with applying the configuration in proper order.
### Configuration
```yaml
has_flash: false # some RouterOS devices Flash directory is
# accessible via explicit /flash/ prefix, for some just /
host: 192.168.1.1 # IP of the Mikrotik device, can be overriden with --override-id CLI argument
include_dirs: # search paths for templates including
- common/
variables: # additonal Jinja2 variables
admin_pass: "pass"
```
### Reset configuration and apply
```shell
cd example/
python ../mikrotik_configurator [--dry-run] --reset *.rsc
```
### Part of the configuration re-applying
```shell
cd example/
python ../mikrotik_configurator [--dry-run] 3-port-forwarding.rsc
```
# Examples
## Setting admin password from config file
File `config.yaml`
```yaml
variables:
admin_pass: "pass"
```
File `1-example.rsc`
```text
/user set admin password="{{ admin_pass }}"
```
## SSH public key loading
File `1-example.rsc`
```text
{{ load_file("~/.ssh/id_rsa.pub", "pcl_id_rsa.pub.txt") }}
/user ssh-keys import user=admin public-key-file=pcl_id_rsa.pub.txt
```
## Creating a firewall target with rollback
```text
/ip firewall filter
add chain="user-input" action=jump jump-target="user-input-ntp" comment="NTP rules"
add chain="user-input-ntp" action=accept in-interface-list=LAN protocol=udp dst-port=123 comment="accept NTP (LAN)"
{{ rollback_delete_chain("user-input-ntp") }}
```
## Custom cleanup
```text
/interface ovpn-client add name=my-ovpn \
connect-to=myhost.com port=1190 \
user=$vpnusername \
password=$vpnpassword \
verify-server-certificate=yes \
cipher=aes256
{% call register_cleanup() %}
/interface ovpn-client remove my-ovpn
{% endcall %}
```
## Script escape
```text
/system script
add dont-require-permissions=yes name="ddns-update" owner=admin policy=read,test source={% call escape_string() %}
/tool fetch output=none mode=https url="https://my.own.dynamic.dns.site\?domain={{ public_domain }}" http-method=post http-data="token={{ ddns_token }}"
{% endcall %}
```

42
example/0_0-initial.rsc Normal file
View file

@ -0,0 +1,42 @@
/system identity set name="MT"
/interface bridge
add name="bridge"
/interface ethernet
set [ find default-name=sfp1 ] disabled=yes
/interface list
add name=LAN
add name=WAN
/ip address
add address=192.168.1.1/24 interface="bridge"
/interface list member
add interface="bridge" list=LAN
/interface bridge port
add bridge="bridge" interface=ether2
add bridge="bridge" interface=ether3
add bridge="bridge" interface=ether4
add bridge="bridge" interface=ether5
add bridge="bridge" interface=ether6
add bridge="bridge" interface=ether7
add bridge="bridge" interface=ether8
add bridge="bridge" interface=ether9
add bridge="bridge" interface=ether10
/user set admin password="{{ admin_pass }}"
#######################
# SSH
#######################
{{ load_file("host-keys/ssh_host_private_key_dsa", "ssh_host_private_key_dsa.txt") }}
{{ load_file("host-keys/ssh_host_private_key_rsa", "ssh_host_private_key_rsa.txt") }}
/ip ssh import-host-key private-key-file=ssh_host_private_key_dsa.txt
/ip ssh import-host-key private-key-file=ssh_host_private_key_rsa.txt
{{ load_file("~/.ssh/id_rsa.pub", "pcl_id_rsa.pub.txt") }}
/user ssh-keys import user=admin public-key-file=pcl_id_rsa.pub.txt

1
example/0_1-security.rsc Normal file
View file

@ -0,0 +1 @@
{% include 'security.rsc' %}

1
example/0_2-firewall.rsc Normal file
View file

@ -0,0 +1 @@
{% include 'firewall_router.rsc' %}

11
example/1-logging.rsc Normal file
View file

@ -0,0 +1,11 @@
/system logging action remove [find default=no]
/system logging remove [find default=no]
/system logging action
add name=pc2 remote=192.168.1.2 remote-port=1514 target=remote
/system logging
add action=pc2 prefix=MT topics=critical
add action=pc2 prefix=MT topics=error
add action=pc2 prefix=MT topics=warning
add action=pc2 prefix=MT topics=info

13
example/2-ntp.rsc Normal file
View file

@ -0,0 +1,13 @@
/system ntp client
set enabled=yes primary-ntp=162.159.200.123 secondary-ntp=162.159.200.1
/system ntp server
set enabled=yes
/ip firewall filter
add chain="user-input" action=jump jump-target="user-input-ntp" comment="NTP rules"
add chain="user-input-ntp" \
action=accept \
in-interface-list=LAN protocol=udp dst-port=123 \
comment="accept NTP (LAN)"
{{ rollback_delete_chain("user-input-ntp") }}

View file

@ -0,0 +1,7 @@
/ip firewall nat
add chain="user-dstnat" action=jump jump-target="user-dstnat-port-forwarding" comment="port forwarding"
add chain="user-dstnat-port-forwarding" action=dst-nat comment="port forwarding -> HTTP" dst-port=80,443 in-interface=ether1-WAN protocol=tcp to-addresses=192.168.1.2
add chain="user-dstnat-port-forwarding" action=dst-nat comment="port forwarding -> SSH" dst-port=1234 in-interface-list=WAN protocol=tcp to-addresses=192.168.1.2 to-ports=22322
{{ rollback_delete_chain("user-dstnat-port-forwarding") }}

6
example/README.md Normal file
View file

@ -0,0 +1,6 @@
# Important notes
* These example files are provided **ONLY** as an example of project structure.
Although it contains a lot of solutions from my configuration files,
it is not meant to be used as an example Mikrotik configuration,
* Example generated SSH keys **SHOULD NOT** be used in your project.

View file

@ -0,0 +1,83 @@
:do {
/ip firewall filter
remove [find chain=input]
remove [find chain=forward action!=passthrough]
remove [find chain=output]
remove [find chain=icmp]
remove [find jump-target="user-input"]
remove [find jump-target="user-forward"]
remove [find jump-target="user-output"]
remove [find chain="core-icmp"]
/ip firewall nat
remove [find chain=srcnat]
remove [find chain=dstnat]
} on-error={}
/ip firewall filter
# INPUT
add chain=input action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
add chain=input action=drop comment="drop connection-state=invalid" connection-state=invalid
add chain=input action=drop comment="drop banned" src-address-list=bans
add chain=input action=jump comment="check ICMP" jump-target="core-icmp" protocol=icmp
add chain=input action=accept comment="accept SSH and HTTP (LAN only)" in-interface-list=LAN protocol=tcp dst-port=22,80
add chain=input action=accept comment="accept WinBox and API (LAN only)" in-interface-list=LAN protocol=tcp dst-port=8291,8728
add chain=input action=accept comment="accept DNS and DHCP (LAN only)" in-interface-list=LAN protocol=udp dst-port=53,67,68
# add chain=input action=accept comment="accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add chain=input action=jump jump-target="user-input" comment="forward to user-input"
add chain=input action=drop comment="drop all not coming from LAN" in-interface-list=!LAN
add chain=input action=drop comment="drop all other"
# FORWARD
add chain=forward action=fasttrack-connection comment="fasttrack" connection-state=established,related
add chain=forward action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
add chain=forward action=drop comment="drop connection-state=invalid" connection-state=invalid
add chain=forward action=drop comment="drop banned" src-address-list=bans
add chain=forward action=jump comment="check ICMP" jump-target="core-icmp" src-address-list=lan in-interface-list=LAN protocol=icmp
add chain=forward action=accept comment="accept DSTNATed from WAN to LAN" connection-state=new in-interface-list=WAN out-interface-list=LAN dst-address-list=lan connection-nat-state=dstnat
add chain=forward action=accept comment="accept all from LAN to WAN" connection-state=new in-interface-list=LAN src-address-list=lan out-interface-list=WAN
add chain=forward action=accept comment="accept all between LAN interfaces" connection-state=new in-interface-list=LAN src-address-list=lan out-interface-list=LAN dst-address-list=lan
# add chain=forward action=accept comment="accept in ipsec policy" ipsec-policy=in,ipsec
# add chain=forward action=accept comment="accept out ipsec policy" ipsec-policy=out,ipsec
add chain=forward action=jump jump-target="user-forward" comment="forward to user-forward"
add chain=forward action=drop comment="drop all other"
# OUTPUT
add chain=output action=jump jump-target="user-output" comment="forward to user-output"
add chain="core-icmp" protocol=icmp icmp-options=0:0 action=accept comment="echo reply"
add chain="core-icmp" protocol=icmp icmp-options=3:0 action=accept comment="net unreachable"
add chain="core-icmp" protocol=icmp icmp-options=3:1 action=accept comment="host unreachable"
add chain="core-icmp" protocol=icmp icmp-options=3:4 action=accept comment="host unreachable fragmentation required"
add chain="core-icmp" protocol=icmp icmp-options=8:0 action=accept comment="allow echo request"
add chain="core-icmp" protocol=icmp icmp-options=11:0 action=accept comment="allow time exceed"
add chain="core-icmp" protocol=icmp icmp-options=12:0 action=accept comment="allow parameter bad"
add chain="core-icmp" action=drop comment="deny all other types"
/ip firewall nat
add chain=srcnat action=masquerade comment="masquerade to WAN" ipsec-policy=out,none out-interface-list=WAN
add chain=srcnat action=jump jump-target="user-srcnat" comment="forward to user-srcnat"
add chain=dstnat action=jump jump-target="user-dstnat" comment="forward to user-dstnat"
# Address lists
/ip firewall address-list
add list=bogons address=0.0.0.0/8 comment="Self-Identification [RFC 3330]"
add list=bogons address=127.0.0.0/8 comment="Loopback [RFC 3330]"
add list=bogons address=10.0.0.0/8 comment="Private[RFC 1918] - CLASS A" disabled=no
add list=bogons address=172.16.0.0/12 comment="Private[RFC 1918] - CLASS B" disabled=no
add list=bogons address=192.168.0.0/16 comment="Private[RFC 1918] - CLASS C" disabled=yes
add list=bogons address=169.254.0.0/16 comment="Link Local [RFC 3330]"
add list=bogons address=192.88.99.0/24 comment="6to4 Relay Anycast [RFC 3068]"
add list=bogons address=198.18.0.0/15 comment="NIDB Testing"
add list=bogons address=192.0.2.0/24 comment="Reserved - IANA - TestNet1"
add list=bogons address=198.51.100.0/24 comment="Reserved - IANA - TestNet2"
add list=bogons address=203.0.113.0/24 comment="Reserved - IANA - TestNet3"
add list=bogons address=224.0.0.0/4 comment="MC, Class D, IANA" disabled=no
{% call register_cleanup() %}
/ip firewall address-list remove [find list=bogons]
{% endcall %}

View file

@ -0,0 +1,59 @@
:do {
/ip firewall filter
remove [find chain=input]
remove [find chain=forward action!=passthrough]
remove [find chain=output]
remove [find chain=icmp]
remove [find jump-target="user-input"]
remove [find jump-target="user-forward"]
remove [find jump-target="user-output"]
remove [find chain="core-icmp"]
/ip firewall nat
remove [find chain=srcnat]
remove [find chain=dstnat]
} on-error={}
/ip firewall filter
# INPUT
add chain=input action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
add chain=input action=drop comment="drop connection-state=invalid" connection-state=invalid
add chain=input action=drop comment="drop banned" src-address-list=bans
add chain=input action=jump comment="check ICMP" jump-target="core-icmp" protocol=icmp
add chain=input action=accept comment="accept SSH and HTTP (LAN only)" in-interface-list=LAN protocol=tcp dst-port=22,80
add chain=input action=accept comment="accept WinBox and API (LAN only)" in-interface-list=LAN protocol=tcp dst-port=8291,8728
# add chain=input action=accept comment="accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add chain=input action=jump jump-target="user-input" comment="forward to user-input"
add chain=input action=drop comment="drop all not coming from LAN" in-interface-list=!LAN
add chain=input action=drop comment="drop all other"
# FORWARD
add chain=forward action=jump jump-target="user-forward" comment="forward to user-forward"
add chain=forward action=drop comment="drop all other"
# OUTPUT
add chain=output action=jump jump-target="user-output" comment="forward to user-output"
add chain="core-icmp" protocol=icmp icmp-options=0:0 action=accept comment="echo reply"
add chain="core-icmp" protocol=icmp icmp-options=3:0 action=accept comment="net unreachable"
add chain="core-icmp" protocol=icmp icmp-options=3:1 action=accept comment="host unreachable"
add chain="core-icmp" protocol=icmp icmp-options=3:4 action=accept comment="host unreachable fragmentation required"
add chain="core-icmp" protocol=icmp icmp-options=8:0 action=accept comment="allow echo request"
add chain="core-icmp" protocol=icmp icmp-options=11:0 action=accept comment="allow time exceed"
add chain="core-icmp" protocol=icmp icmp-options=12:0 action=accept comment="allow parameter bad"
add chain="core-icmp" action=drop comment="deny all other types"

View file

@ -0,0 +1,24 @@
{ :local ver [/system resource get version]; :global vermajor [:pick $ver 0 [:find $ver "."]] }
/ip neighbor discovery-settings set discover-interface-list=none
/ip ipsec policy set 0 disabled=yes
:if ($vermajor = 7) do={ /ipv6 settings set disable-ipv6=yes }
:if ($vermajor = 6) do={ /system package disable ipv6 }
/tool mac-server set allowed-interface-list=none
/tool mac-server mac-winbox set allowed-interface-list=none
/tool mac-server ping set enabled=no
/ip service set api disabled=yes
/ip service set api-ssl disabled=yes
/ip service set ftp disabled=yes
/ip service set telnet disabled=yes
/ip service set winbox disabled=no
/tool bandwidth-server set enabled=no
/ip ssh set strong-crypto=yes host-key-size=4096 forwarding-enabled=both always-allow-password-login=yes
/ip settings set rp-filter={{ rp_filter | default("strict") }} secure-redirects=no tcp-syncookies=yes

9
example/config.yml Normal file
View file

@ -0,0 +1,9 @@
has_flash: false
host: 192.168.1.1
include_dirs:
- common/
variables:
admin_pass: "pass"

View file

@ -0,0 +1,9 @@
-----BEGIN PRIVATE KEY-----
MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAI3m06Sl+2TgMzx9an3BRwuMD56J
VrrWrTlVdcNYaxgquiv3osjFHs+kEAn8jY+pPFKCM9lpfoTj8FT0qPkKtsZ+LcB9
YrxS4bsW8LGvnQScpZcrqFze4Ec0AF+7vYhP9pBHESlxlgEsOIDZvBsVErS/U7WM
wgnIgrY0e/i2GaJZAhUAi0LIo6w4dKsjKCMN/j6X5/1YKAkCgYA8Hr5VfkCNYbMf
J7amr+SilmkBLuQUn0+pV4FvGdPCa9EY0gxCP+0N82aintU7HfqOqE9pdtwFrbXU
+/GyXJIuNME3y4JAvdFkJ18vAUVM0+7rGQ22BxdwrkBT3DUXV+9xjkYAh+6mpWOf
S6Iyga1TE3nNlHtaP6KgBS7tdlcBxwQWAhRurYPs9TMeP1mlge45J3hbBO4Jbg==
-----END PRIVATE KEY-----

View file

@ -0,0 +1 @@
ssh-dss AAAAB3NzaC1kc3MAAACBAI3m06Sl+2TgMzx9an3BRwuMD56JVrrWrTlVdcNYaxgquiv3osjFHs+kEAn8jY+pPFKCM9lpfoTj8FT0qPkKtsZ+LcB9YrxS4bsW8LGvnQScpZcrqFze4Ec0AF+7vYhP9pBHESlxlgEsOIDZvBsVErS/U7WMwgnIgrY0e/i2GaJZAAAAFQCLQsijrDh0qyMoIw3+Ppfn/VgoCQAAAIA8Hr5VfkCNYbMfJ7amr+SilmkBLuQUn0+pV4FvGdPCa9EY0gxCP+0N82aintU7HfqOqE9pdtwFrbXU+/GyXJIuNME3y4JAvdFkJ18vAUVM0+7rGQ22BxdwrkBT3DUXV+9xjkYAh+6mpWOfS6Iyga1TE3nNlHtaP6KgBS7tdlcBxwAAAIBFHqQTevlpTRONMZ40oxL4/DOz8If1ja7WTx4hH3HXWtvDEZii57FDv/xpUkCpP0dqp4NcaPWxZ5XH8+mdKR3zPxD8L+1S4NnleoPFtSMQU5v/Zcq7R79FGC0kaocjQN/a2XQOzifaeXx0nSD49l36bpkkMXYCBH0/l9HOD3xRkg== krystiand@PCL

View file

@ -0,0 +1,40 @@
-----BEGIN PRIVATE KEY-----
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
k438geLC6lhQaxj0UMS1/PvURQ==
-----END PRIVATE KEY-----

View file

@ -0,0 +1,11 @@
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA87rfcjP7LwChfd4uvxOx
8ZJw3SnUWQ0uL2I2gZIhIPmyiOGmx/z3gzdN/aI7px0u9a4aDs2lNAlN3B+a/8n9
Bu7d8k42fhwLcHV/o2VBiExRKxE7XgHGK8G/2heaMT+ivdr4eRH8o8sAHfZeJ/2W
I6Md2/9lL5+wuNvmpbUnxDpF0bbE36PRp5JEWBhT8gvlN0Tdxlz9uBpu1tmQ0ZoN
Ya1JFWjRqsc/qMgslBXr7KY0clwzJp4cBvCZevO/yZXeDjpoQ/vjL/KlF8WlTqxe
WnsvRz2byQiJLXqRTT1V3cdX9M6Zqu1FPHZIISvdP+JMaBhXuU7fSsYeLPYwCnUU
YQop012Servr6wwd9Bb+bUb28GtXheUddo02ry0g7yo0+GjAVw71yzugfnZLxIHa
xTK2j/4rljfRH3L+Mx/8/FGBOddKLth/hcI0QUq6kTs59sGjbGNZyx6A4XD93K8e
shwALGu8cVEreaobdmiahctD5wiONY/qqQhPlWk+l6TtAgMBAAE=
-----END PUBLIC KEY-----

14
intellij/README.md Normal file
View file

@ -0,0 +1,14 @@
## Configuration dialog
<img src="config.jpg" alt="config" />
## Keywords
* Tab 1 - [keywords-1.txt](keywords-1.txt)
* Tab 2 - [keywords-2.txt](keywords-2.txt)
* Tab 3 - [keywords-3.txt](keywords-3.txt)
* Tab 4 - [keywords-4.txt](keywords-4.txt)
## Highlighting example
<img src="highlight.jpg" alt="highlight" />

BIN
intellij/config.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
intellij/highlight.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

4
intellij/keywords-1.txt Normal file
View file

@ -0,0 +1,4 @@
:delay
:do
:local
:put

5
intellij/keywords-2.txt Normal file
View file

@ -0,0 +1,5 @@
add
find
remove
set
disable

54
intellij/keywords-3.txt Normal file
View file

@ -0,0 +1,54 @@
/certificate
/certificate import
/file
/file print
/interface
/interface bridge
/interface bridge filter
/interface bridge port
/interface ethernet
/interface ethernet
/interface list
/interface list member
/interface ovpn-client
/interface ovpn-client remove
/interface ovpn-server
/interface vlan
/interface wireless
/interface wireless security-profiles
/ip
/ip address
/ip dhcp-server
/ip dhcp-server lease
/ip dhcp-server network
/ip dns
/ip dns static
/ip firewall
/ip firewall address-list
/ip firewall filter
/ip firewall nat
/ip firewall nat
/ip ipsec
/ip ipsec policy
/ip neighbor
/ip neighbor discovery-settings
/ip pool
/ip route
/ip service
/ip settings
/ip ssh
/ip ssh import-host-key
/lcd pin
/ppp profile
/ppp secret
/system identity
/system logging
/system logging action
/system package
/system scheduler
/system script
/tool bandwidth-server
/tool fetch
/tool mac-server
/user
/user ssh-keys import

99
intellij/keywords-4.txt Normal file
View file

@ -0,0 +1,99 @@
action
address
address-pool
allow-remote-requests
allowed-interface-list
auth
authentication-types
band
band
bridge
cache-size
certificate
chain
channel-width
cipher
client-id
comment
connect-to
connection-nat-state
connection-state
contents
default
default-forwarding
default-name
default-profile
disabled
discover-interface-list
dns-server
dont-require-permissions
dst-address
dst-port
enabled
file
frequency
gateway
host-key-size
http-data
http-method
in-interface
in-interface-list
interface
interval
ipsec-policy
jump-target
list
local-address
mac-address
management-protection
master-interface
mode
name
on-error
on-event
out-interface
out-interface-list
output
owner
passphrase
password
pin-number
poe-out
policy
port
prefix
private-key-file
profile
protocol
public-key-file
pvid
ranges
remote
remote-address
remote-port
rp-filter
secure-redirects
security-profile
server
servers
service
skip-dfs-channels
source
src-address
ssid
start-date
start-time
strong-crypto
supplicant-identity
target
tcp-syncookies
to-addresses
to-ports
topics
ttl
url
user
verify-server-certificate
vlan-id
wpa2-pre-shared-key
wps-mode

View file

View file

@ -0,0 +1,106 @@
import argparse
import os
import subprocess
import tempfile
import yaml
import generator
from utils import query_yes_no
def main():
argparser = argparse.ArgumentParser()
argparser.add_argument('-c', '--config', default="config.yml", type=str, metavar="PATH")
argparser.add_argument('-n', '--dry-run', action='store_true')
argparser.add_argument('--reset', action='store_true')
argparser.add_argument('--override-ip', type=str)
argparser.add_argument('files', type=str, nargs="+", metavar="NAME")
args = argparser.parse_args()
dry_run = args.dry_run
cfg = yaml.load(open(args.config, "rt"), Loader=yaml.FullLoader)
host = cfg["host"]
has_flash = cfg.get("has_flash", False)
if args.override_ip is not None:
host = args.override_ip
files = args.files
orders = [float(os.path.basename(x).split("-")[0].replace("_", ".")) for x in files]
if orders != list(sorted(orders)):
print("mixed up order")
exit(1)
if args.reset and orders[0] != 0:
print("reset must start with 0_0")
exit(1)
if not args.reset and orders[0] == 0:
print("not reset can't start with 0_0")
exit(1)
if not dry_run and args.reset:
if not query_yes_no("Are you sure you want to reset configuration?", "no"):
exit(1)
def gen(x):
s = f'\n/log info message="starting {x}..."\n'
s += generator.render_file(x, cfg.get("include_dirs", []), cfg.get("variables", {}))
s += f'\n/log info message="finished {x}"\n'
return s
script = "\n".join(gen(x) for x in files)
if args.reset:
script = ":delay 7s\n" + script
script += "\n/log info message=\"CONFIGURATION DONE\"\n"
base_path = "flash/" if has_flash else ""
script_name = "output.rsc"
print(script)
with tempfile.NamedTemporaryFile(mode="wt") as f:
f.write(script)
f.flush()
cargs = [
"scp",
"-o", "StrictHostKeyChecking=false",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "PubkeyAcceptedKeyTypes=+ssh-rsa",
f.name,
f"admin@{host}:{base_path}{script_name}"
]
print(" ".join(cargs))
if not dry_run:
subprocess.check_call(cargs)
if args.reset:
cmd = f"/system reset-configuration no-defaults=yes skip-backup=yes run-after-reset={base_path}{script_name}"
else:
cmd = f"/import file={base_path}{script_name}"
cargs = [
"ssh",
"-o", "StrictHostKeyChecking=false",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "PubkeyAcceptedKeyTypes=+ssh-rsa",
f"admin@{host}",
cmd,
]
print(" ".join(cargs))
if not dry_run:
if args.reset:
subprocess.run(cargs)
else:
out = subprocess.check_output(cargs).decode("utf-8")
if "Script file loaded and executed successfully" not in out:
print("Script error")
exit(1)
main()

View file

@ -0,0 +1,89 @@
import os
from textwrap import indent
from typing import Dict, List
from jinja2 import Environment, FileSystemLoader
from utils import read_text_file
script_dir = os.path.dirname(os.path.realpath(__file__))
cleanups = []
def escape_for_mikrotik(cnt):
return cnt \
.replace('\\', '\\\\') \
.replace("\t", "\\t") \
.replace("\r", "\\r") \
.replace("\n", "\\n") \
.replace('"', '\\"')
def load_file(path, name):
_, ext = os.path.splitext(name)
assert ext == ".txt"
file_cnt = escape_for_mikrotik(read_text_file(os.path.expanduser(path)).strip())
esc = escape_for_mikrotik(file_cnt)
cnt = f"""
:execute script=":put \\"{esc}\\"" file="{name}"
:delay 1000ms
"""
return cnt
def generate_catch_block(body):
return f""":do {{
{indent(body.strip(), " ")}
}} on-error={{}}
"""
def register_cleanup(caller):
body = caller()
body = generate_catch_block(body)
cleanups.insert(0, body)
return ""
def escape_string(caller):
body = caller().strip()
return f'"{escape_for_mikrotik(body)}"'
def rollback_delete_chain(name):
body = generate_catch_block(f"""
/ip firewall filter remove [find chain="{name}"]
/ip firewall filter remove [find jump-target="{name}"]
/ip firewall nat remove [find chain="{name}"]
/ip firewall nat remove [find jump-target="{name}"]
/ip firewall mangle remove [find chain="{name}"]
/ip firewall mangle remove [find jump-target="{name}"]
""")
cleanups.insert(0, body)
return ""
def render_file(path: str, include_dirs: List[str], variables: Dict[str, str]):
global cleanups
cleanups = []
env = Environment(
loader=FileSystemLoader([
os.path.join("."),
*include_dirs,
]),
)
env.globals['register_cleanup'] = register_cleanup
env.globals['escape_string'] = escape_string
env.globals['rollback_delete_chain'] = rollback_delete_chain
env.globals = {**env.globals, **variables}
content = env.get_template(os.path.basename(path))
content = content.render(load_file=load_file)
content = "\n".join(cleanups) + "\n\n" + content
return content

View file

@ -0,0 +1,35 @@
import sys
def write_text_file(path, content):
with open(path, "wt", encoding="utf-8") as f:
f.write(content)
def read_text_file(path):
with open(path, "rt") as f:
return f.read()
def query_yes_no(question, default="yes"):
valid = {"yes": True, "y": True, "ye": True,
"no": False, "n": False}
if default is None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
elif default == "no":
prompt = " [y/N] "
else:
raise ValueError("invalid default answer: '%s'" % default)
while True:
sys.stdout.write(question + prompt)
choice = input().lower()
if default is not None and choice == '':
return valid[default]
elif choice in valid:
return valid[choice]
else:
sys.stdout.write("Please respond with 'yes' or 'no' "
"(or 'y' or 'n').\n")

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
jinja2
pyyaml

4
tools/gen_ssh_host_keys.sh Executable file
View file

@ -0,0 +1,4 @@
ssh-keygen -N "" -f ssh_host_private_key_dsa -t dsa -m pkcs8
ssh-keygen -N "" -f ssh_host_private_key_rsa -t rsa -m pkcs8
ssh-keygen -e -f ssh_host_private_key_rsa.pub -m pkcs8 > ssh_host_private_key_rsa.pub.tmp
mv ssh_host_private_key_rsa.pub.tmp ssh_host_private_key_rsa.pub