feat: Add initial scripts and configuration for automated BGP routing deployment to MikroTik devices.

This commit is contained in:
2026-02-26 10:28:50 +08:00
parent d426b15d9f
commit a57ea1aa7d
5 changed files with 429 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
venv/
.env
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git/
# Project specific
routing-lokal.rsc
bgp_sync.log

133
bgp-router.rsc Normal file
View File

@@ -0,0 +1,133 @@
# 2026-02-26 08:29:06 by RouterOS 7.12.2
# software id = 2U0N-G8EU
#
# model = CCR2216-1G-12XS-2XQ
# serial number = HFA098NA23B
/interface ethernet
set [ find default-name=sfp28-1 ] advertise=\
10G-baseT,10G-baseSR-LR,10G-baseCR comment=to_core_router
set [ find default-name=sfp28-2 ] auto-negotiation=no comment=FS speed=\
10G-baseSR-LR
set [ find default-name=sfp28-3 ] comment=To_CCR1009
/interface eoip
add disabled=yes local-address=125.208.136.205 mac-address=02:D7:B4:33:3A:D0 \
name=eoip200-ToNix remote-address=103.109.158.23 tunnel-id=200
/interface vlan
add comment=AS136106 interface=sfp28-2 name=vlan245 vlan-id=245
add comment="Dimensi Site Kuta CBN" interface=sfp28-2 name=vlan3912 vlan-id=\
3912
add interface=sfp28-1 name=vlan_99_ptp vlan-id=99
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip pool
add name=pool1 ranges=103.138.63.179-103.138.63.182
add name=pool2 ranges=10.10.0.10-10.10.0.254
/port
set 0 name=serial0
/ppp profile
set *0 dns-server=202.158.3.7
/routing bgp template
add address-families=ip as=138843 disabled=no input.filter=in-Iptransit name=\
GLS-IPT nexthop-choice=force-self output.filter-chain=out-ipTransit \
.network=Ip-GLS router-id=103.87.186.155 routing-table=main
add address-families=ip as=138843 disabled=no input.filter=in-Konten \
multihop=no name=GLS-Konten output.filter-chain=out-Konten .network=\
Ip-GLS router-id=125.208.136.205 routing-table=main
add address-families=ip as=138843 disabled=no input.filter=IN-GLS name=To-NIX \
output.filter-chain=OUT-GLS .network=Ip-GLS .redistribute=connected \
router-id=103.138.63.69 routing-table=main
/ip neighbor discovery-settings
set discover-interface-list=!dynamic
/ip settings
set accept-source-route=yes
/ip address
add address=125.208.136.205/31 interface=vlan3912 network=125.208.136.204
add address=103.87.186.155 interface=vlan245 network=103.87.186.154
add address=103.138.63.65/30 comment="IP Disribusi GLS" interface=vlan_99_ptp \
network=103.138.63.64
add address=103.138.63.69 comment="IP PTP NIX" disabled=yes interface=\
eoip200-ToNix network=103.138.63.68
add address=10.254.1.2/24 disabled=yes interface=eoip200-ToNix network=\
10.254.1.0
/ip dns
set servers=202.158.3.7,202.158.3.6
/ip firewall address-list
add address=103.138.63.0/24 list=Ip-GLS
/ip route
add disabled=yes distance=1 dst-address=0.0.0.0/0 gateway=125.208.136.204 \
pref-src="" routing-table=main scope=30 suppress-hw-offload=no \
target-scope=10
add disabled=yes distance=2 dst-address=0.0.0.0/0 gateway=103.87.186.154 \
pref-src="" routing-table=main scope=30 suppress-hw-offload=no \
target-scope=10
add disabled=yes distance=1 dst-address=103.138.62.0/24 gateway=10.254.1.1 \
pref-src="" routing-table=main scope=30 suppress-hw-offload=no \
target-scope=10
add dst-address=103.138.63.0/24 gateway=103.138.63.66
/ip service
set telnet disabled=yes
set ftp disabled=yes
set www address=103.138.63.0/24
set ssh disabled=yes
set api disabled=yes
set api-ssl disabled=yes
/ipv6 nd
set [ find default=yes ] advertise-mac-address=no disabled=yes
/ppp secret
add disabled=yes local-address=103.138.63.65 name=ogeb password=network135 \
remote-address=103.138.63.66
/routing bgp connection
add address-families=ip as=138843 connect=yes disabled=no input.filter=\
in-Iptransit listen=yes local.address=103.87.186.155 .role=ebgp multihop=\
no name=FS-IPtansit nexthop-choice=force-self output.filter-chain=\
out-ipTransit .network=Ip-GLS .redistribute=connected remote.address=\
103.87.186.154/32 .as=136106 router-id=103.87.186.155 routing-table=main \
templates=GLS-IPT
add address-families=ip as=138843 connect=yes disabled=no input.filter=\
in-Konten listen=yes local.role=ebgp multihop=no name=FS-CDN \
output.filter-chain=out-Konten .network=Ip-GLS remote.address=\
125.208.136.204/32 .as=4787 router-id=125.208.136.205 routing-table=main \
templates=GLS-Konten
add address-families=ip as=138843 connect=yes disabled=yes input.filter=\
IN-GLS listen=yes local.role=ibgp name=GLS-to-NIX output.filter-chain=\
OUT-GLS .network=Ip-GLS .redistribute=connected,static remote.address=\
103.138.63.68/32 .as=138843 router-id=103.138.63.69 routing-table=main \
templates=To-NIX
/routing filter rule
add chain=in-Iptransit comment=IPTansit disabled=no rule=\
"if (dst-len <= 7 ) { reject; }"
add chain=in-Iptransit disabled=no rule="if ( dst-len in 8-24 ) { accept; }"
add chain=in-Iptransit disabled=no rule="if ( dst-len > 25 ) { reject; }"
add chain=out-ipTransit disabled=no rule=\
"if ( dst in 103.138.63.0/24 ) { accept;}"
add chain=in-Konten comment="Rule Konten" disabled=no rule=\
"if (dst-len <= 7 ) { reject; }"
add chain=in-Konten disabled=no rule="if (dst-len in 8-24 ) { set bgp-local-pr\
ef 800; set distance -5; accept}"
add chain=in-Konten disabled=no rule="if ( dst-len >= 25 ) { reject; }"
add chain=out-Konten disabled=no rule=\
"if ( dst in 103.138.63.0/24 ) {accept;}"
add chain=IN-GLS comment="IN GLS" disabled=no rule=\
"if ( dst-len in 25-32 && dst in 103.138.63.0/24 ) { accept; }"
add chain=IN-GLS disabled=yes rule=\
"if ( dst in 103.138.63.0/24 ) { accept; }"
add chain=OUT-GLS disabled=no rule=\
"if ( dst in 103.138.63.0/24 ) { set bgp-local-pref 600 ; accept; }"
/system clock
set time-zone-autodetect=no time-zone-name=Asia/Makassar
/system identity
set name=GLS-Core-Sukawati
/system note
set show-at-login=no
/system package update
set channel=long-term
/system routerboard settings
set enter-setup-on=delete-key
/system scheduler
add name=schedule1 on-event=/system/reboot policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
start-date=2025-07-21 start-time=03:00:00
/tool graphing interface
add interface=sfp28-2
add interface=vlan245
add interface=vlan3912

141
deploy_routes.py Normal file
View File

@@ -0,0 +1,141 @@
import os
import requests
import json
import traceback
import paramiko
from scp import SCPClient
def get_routers_from_config(config_path, isp_name="dimensi"):
"""Mengambil list router dari config.json billing-mcp"""
try:
with open(config_path, 'r') as f:
config = json.load(f)
isp_data = config.get("isps", {}).get(isp_name)
if not isp_data:
print(f"ISP {isp_name} tidak ditemukan dalam config.")
return []
routers = isp_data.get("routers", {})
router_list = []
for name, data in routers.items():
if name.startswith(f"router-{isp_name}"):
router_list.append({
"name": name,
"host": data.get("host"),
"port": data.get("port"),
"user": data.get("user"),
"pass": data.get("pass")
})
return router_list
except Exception as e:
print(f"Error membaca config.json: {e}")
return []
def deploy_via_rest_api(router, rsc_filepath="routing-lokal.rsc"):
"""Mengirim rute IP satu per satu via REST API Session"""
host = router["host"]
user = router["user"]
password = router["pass"]
name = router["name"]
port = router["port"]
api_url = f"http://{host}/rest"
if port != 80:
api_url = f"http://{host}:{port}/rest"
print(f"\n--- Memproses {name} [{host}:{port}] via API ---")
if not os.path.exists(rsc_filepath):
print(f"Error: {rsc_filepath} tidak ditemukan!")
return
# Parsing list name dan IPs dari file rsc
ips = []
list_name = "ip-lokal" # Default
with open(rsc_filepath, 'r') as f:
for line in f:
line = line.strip()
if line.startswith("remove [find"):
import re
match = re.search(r'list="([^"]+)"', line)
if match:
list_name = match.group(1)
elif line.startswith("add list="):
import re
match = re.search(r'address="([^"]+)"', line)
if match:
ips.append(match.group(1))
if not ips:
print("Tidak ada IP yang ditemukan di dalam file RSC.")
return
print(f"Ditemukan {len(ips)} subnet untuk dimasukkan pada list '{list_name}'.")
session = requests.Session()
session.auth = (user, password)
session.verify = False
# 1. Menghapus list lama di router
print(f"Menghapus address-list '{list_name}' lama di router...")
del_payload = {"script": f"/ip/firewall/address-list/remove [find list=\"{list_name}\"]"}
try:
res_del = session.post(f"{api_url}/execute", json=del_payload, timeout=30)
if res_del.status_code not in (200, 201):
print(f"Warning saat menghapus list (abaikan jika baru): {res_del.text}")
except Exception as e:
print(f"Warning execute remove list: {e}")
# Beri waktu luang agar RouterOS mengeksekusi penghapusan di background
import time
time.sleep(2)
# 2. Add IP lewat eksekusi Batch Script (Mencegah Payload Too Large)
print(f"Mengirim {len(ips)} address ke router secara batch... (±500 list per request)")
success_count = 0
fail_count = 0
start_time = time.time()
chunk_size = 500
for i in range(0, len(ips), chunk_size):
chunk_ips = ips[i:i + chunk_size]
# Buat kumpulan string baris perintah MikroTik dibungkus error handler
# agar jika ada 1 IP invalid, tidak menghentikan keseluruhan 500 IP lainnya (abort script)
commands = [f"do {{ /ip/firewall/address-list/add list={list_name} address={ip} }} on-error={{}}" for ip in chunk_ips]
script_code = "\n".join(commands)
payload = {
"script": script_code
}
try:
# Karena execution berjalan di background, kita bisa beri jeda aman antar batch
res = session.post(f"{api_url}/execute", json=payload, timeout=30)
if res.status_code in (200, 201):
success_count += len(chunk_ips)
else:
fail_count += len(chunk_ips)
print(f"Error pada batch {i}-{i+chunk_size}: {res.text}")
time.sleep(0.5) # Hindari CPU router Spike 100%
except Exception as e:
fail_count += len(chunk_ips)
print(f"Exception batch {i}-{i+chunk_size}: {e}")
print(f"Progress batch terkirim: {min(i+chunk_size, len(ips))}/{len(ips)}...")
elapsed = time.time() - start_time
print(f"Selesai! {name} terupdate dalam {elapsed:.1f} dtk.")
print(f"Berhasil: {success_count} IPs, Gagal: {fail_count} IPs.")
if __name__ == "__main__":
config_path = "/home/wartana/myApp/billing-mcp/config.json"
routers = get_routers_from_config(config_path, "dimensi")
if routers:
print(f"Ditemukan {len(routers)} router distribusi dimensi.")
for r in routers:
deploy_via_rest_api(r, "routing-lokal.rsc")
else:
print("Tidak ada router target di config.")

23
run_bgp_sync.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# AUTO-SYNC BGP ROUTING TO DISTRIBUTION ROUTERS
# Dijalankan via Cron Job
cd /home/wartana/myApp/iix/
source venv/bin/activate
echo "=================================================="
echo "Memulai Sinkronisasi BGP: $(date)"
# 1. Ekstrak rute BGP Lokal dari Core (menghasilkan routing-lokal.rsc)
python3 sync_routing.py
if [ $? -eq 0 ]; then
echo "Ekstraksi berhasil. Memulai deployment ke router distribusi..."
# 2. Upload batch execute ke Router Distribusi
python3 deploy_routes.py
else
echo "Gagal mengekstrak rute BGP lokal. Deployment dibatalkan."
fi
echo "Selesai: $(date)"
echo "=================================================="

109
sync_routing.py Normal file
View File

@@ -0,0 +1,109 @@
import os
import requests
import json
import ipaddress
from dotenv import load_dotenv
# Load configuration dari file .env
load_dotenv()
BGP_ROUTER_IP = os.getenv("BGP_ROUTER_IP")
BGP_ROUTER_USER = os.getenv("BGP_ROUTER_USER")
BGP_ROUTER_PASSWORD = os.getenv("BGP_ROUTER_PASSWORD")
# URL API RouterOS v7 untuk membaca tabel routing
API_URL = f"http://{BGP_ROUTER_IP}/rest/routing/route"
# Kredensial basic auth
auth = (BGP_ROUTER_USER, BGP_ROUTER_PASSWORD)
def get_local_bgp_routes():
"""Mengambil rentang IP Lokal/CDN dari tabel routing BGP secara efisien menggunakan parameter query API MikroTik"""
print(f"Menghubungi router {BGP_ROUTER_IP} via REST API...")
all_local_ips = []
try:
# PENGAMBILAN RUTE KONTEN / CDN
# Di RouterOS v7, rute CDN diset distance-nya menjadi -5 dari base eBGP (20), sehingga menjadi 15.
# Kita menggunakan query string "?distance=15&.proplist=dst-address" agar routeros
# hanya me-return rute yang bersangkutan secara instan tanpa perlu meloop 900.000 rute.
print("Mengambil rute Konten / CDN (Distance 15)... Ini dapat memakan waktu hingga satu menit.")
res_cdn = requests.get(
API_URL,
auth=auth,
params={"distance": "15", ".proplist": "dst-address"},
verify=False,
timeout=120
)
if res_cdn.status_code == 200:
cdn_routes = res_cdn.json()
ids = [r.get("dst-address") for r in cdn_routes if r.get("dst-address")]
print(f" -> Berhasil mengambil {len(ids)} rute CDN.")
all_local_ips.extend(ids)
else:
print(f" -> Error CDN: {res_cdn.status_code} - {res_cdn.text}")
# PENGAMBILAN RUTE NIX / LOKAL OpenIXP
# Berdasarkan konfigurasi bgp-router.rsc, peer NIX menggunakan local.role=ibgp (Distance base: 200).
print("Mengambil rute NIX / OpenIXP (Distance 200)... Ini dapat memakan waktu hingga satu menit.")
res_nix = requests.get(
API_URL,
auth=auth,
params={"distance": "200", ".proplist": "dst-address"},
verify=False,
timeout=120
)
if res_nix.status_code == 200:
nix_routes = res_nix.json()
ids = [r.get("dst-address") for r in nix_routes if r.get("dst-address")]
print(f" -> Berhasil mengambil {len(ids)} rute NIX.")
all_local_ips.extend(ids)
else:
print(f" -> Error NIX: {res_nix.status_code} - {res_nix.text}")
# Hapus default routes (0.0.0.0/0 dan ::/0) jika ada
all_local_ips = [ip for ip in all_local_ips if ip not in ("0.0.0.0/0", "::/0")]
return list(set(all_local_ips)) # Kembalikan list unik
except requests.exceptions.RequestException as e:
print(f"Gagal mengambil data dari router via API: {e}")
return []
def generate_address_list_script(route_list, list_name, filename):
"""Membentuk file .rsc yang berisi perintah create address-list MikroTik"""
if not route_list:
print(f"List kosong, tidak ada rute LOKAL yang ditemukan.")
return
print(f"Membuat file {filename} dengan {len(route_list)} subnet IP...")
with open(filename, "w") as f:
f.write(f"# Dibuat otomatis via Script API Python\n")
f.write(f"/ip firewall address-list\n")
# Bersihkan list lama sebelum memasukkan yang baru
f.write(f"remove [find list=\"{list_name}\"]\n")
for ip in route_list:
f.write(f"add list={list_name} address=\"{ip}\"\n")
print(f"File {filename} berhasil dibuat.")
if __name__ == "__main__":
if not all([BGP_ROUTER_IP, BGP_ROUTER_USER, BGP_ROUTER_PASSWORD]):
print("Error: Variabel kredensial/BGP_ROUTER_IP belum diset dengan benar di file .env")
exit(1)
print("--- Memulai Proses Ekstraksi Ringan Routing LOKAL ---")
local_ips = get_local_bgp_routes()
if local_ips:
print(f"\nRingkasan:")
print(f"Total Subnet IP Lokal & CDN: {len(local_ips)}")
# Generate script address-list khusus untuk trafik Lokal
generate_address_list_script(local_ips, "ip-lokal", "routing-lokal.rsc")
print("\nSelesai! File routing-lokal.rsc siap di-upload ke router distribusi.")
print("Di Router Distribusi jalankan perintah: /import file-name=routing-lokal.rsc")