feat: Add initial scripts and configuration for automated BGP routing deployment to MikroTik devices.
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
133
bgp-router.rsc
Normal 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
141
deploy_routes.py
Normal 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
23
run_bgp_sync.sh
Executable 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
109
sync_routing.py
Normal 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")
|
||||
Reference in New Issue
Block a user