diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04865ff --- /dev/null +++ b/.gitignore @@ -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 diff --git a/bgp-router.rsc b/bgp-router.rsc new file mode 100644 index 0000000..f20b78b --- /dev/null +++ b/bgp-router.rsc @@ -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 diff --git a/deploy_routes.py b/deploy_routes.py new file mode 100644 index 0000000..bd8cda6 --- /dev/null +++ b/deploy_routes.py @@ -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.") diff --git a/run_bgp_sync.sh b/run_bgp_sync.sh new file mode 100755 index 0000000..178dffd --- /dev/null +++ b/run_bgp_sync.sh @@ -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 "==================================================" diff --git a/sync_routing.py b/sync_routing.py new file mode 100644 index 0000000..af182ca --- /dev/null +++ b/sync_routing.py @@ -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")