diff --git a/mikrotik-tool/494.rsc b/mikrotik-tool/494.rsc index 92339cd..1ae28dd 100644 --- a/mikrotik-tool/494.rsc +++ b/mikrotik-tool/494.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = K4QG-8NQV # # model = RB5009UG+S+ @@ -468,7 +468,7 @@ add chain=ospf-in disabled=no rule="accept;\r\ add chain=ospf-out disabled=no rule="accept;" /routing ospf interface-template add area=ospf-area-1 auth=sha512 auth-id=1 disabled=no interfaces=\ - ether2-climax type=ptp use-bfd=yes + ether2-climax type=ptp use-bfd=no add area=ospf-area-1 disabled=no passive /routing ospf static-neighbor add address=10.250.1.70%ether2-climax area=ospf-area-1 disabled=no \ diff --git a/mikrotik-tool/982.rsc b/mikrotik-tool/982.rsc index b208b04..c317d47 100644 --- a/mikrotik-tool/982.rsc +++ b/mikrotik-tool/982.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = FUVS-HCM5 # # model = CCR1009-7G-1C-1S+ diff --git a/mikrotik-tool/climax.rsc b/mikrotik-tool/climax.rsc index 6431c54..ba3ff0c 100644 --- a/mikrotik-tool/climax.rsc +++ b/mikrotik-tool/climax.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = UETF-WF31 # # model = CCR2004-16G-2S+ @@ -399,8 +399,6 @@ set api address=204.110.188.0/22,10.0.0.0/8,100.64.0.0/10 set api-ssl certificate=MyCA /ip ssh set host-key-type=ed25519 strong-crypto=yes -/ipv6 address -add address=2606:1c80:0:1010::1 interface=ether6-verona-11ghz /ipv6 dhcp-relay option set client_mac value="0x0001\$(CLIENT_MAC)" /ipv6 nd @@ -436,7 +434,7 @@ add area=backbone-v3 cost=10 disabled=no use-bfd=no add area=backbone-v2 auth=sha512 auth-id=1 cost=10 disabled=no interfaces=\ ether6-verona-11ghz priority=1 type=ptp use-bfd=no add area=backbone-v2 auth=sha512 auth-id=1 cost=10 disabled=no interfaces=\ - ether5-494 priority=1 type=ptp use-bfd=yes + ether5-494 priority=1 type=ptp use-bfd=no add area=backbone-v3 auth=sha512 auth-id=1 cost=10 disabled=no interfaces=\ ether3-culleoka-11ghz priority=1 type=ptp use-bfd=no add area=backbone-v2 auth=sha512 auth-id=1 cost=10 disabled=no interfaces=\ diff --git a/mikrotik-tool/core.rsc b/mikrotik-tool/core.rsc index ec842a8..f53435d 100644 --- a/mikrotik-tool/core.rsc +++ b/mikrotik-tool/core.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = XS5B-41QR # # model = CCR1009-7G-1C-1S+ diff --git a/mikrotik-tool/culleoka.rsc b/mikrotik-tool/culleoka.rsc index a2c6c08..c18a90e 100644 --- a/mikrotik-tool/culleoka.rsc +++ b/mikrotik-tool/culleoka.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = HVP9-3G0K # # model = CCR1009-7G-1C-1S+ diff --git a/mikrotik-tool/edge.rsc b/mikrotik-tool/edge.rsc index 0aa4856..5984b42 100644 --- a/mikrotik-tool/edge.rsc +++ b/mikrotik-tool/edge.rsc @@ -1,4 +1,4 @@ -# may/09/2026 08:51:41 by RouterOS 6.49.18 +# may/09/2026 09:00:22 by RouterOS 6.49.18 # software id = 8XZE-R7EJ # # model = CCR2004-1G-12S+2XS diff --git a/mikrotik-tool/lowry.rsc b/mikrotik-tool/lowry.rsc index 1852b2d..2eae334 100644 --- a/mikrotik-tool/lowry.rsc +++ b/mikrotik-tool/lowry.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = 2I9X-PQZP # # model = CCR1009-7G-1C-1S+ diff --git a/mikrotik-tool/main.go b/mikrotik-tool/main.go index 3e1d813..f743759 100644 --- a/mikrotik-tool/main.go +++ b/mikrotik-tool/main.go @@ -285,17 +285,21 @@ func runExport(cfg *Config) { wg.Wait() } +// findRouter looks up a router by name in the config; returns nil if missing. +func findRouter(cfg *Config, name string) *Router { + for i := range cfg.Routers { + if cfg.Routers[i].Name == name { + r := cfg.Routers[i] + return &r + } + } + return nil +} + // runAPI logs into one router by name and runs an arbitrary API command, // printing the rows returned. Useful for ad-hoc lookups (`arp culleoka`). func runAPI(cfg *Config, routerName string, apiArgs []string) { - var target *Router - for i := range cfg.Routers { - if cfg.Routers[i].Name == routerName { - r := cfg.Routers[i] - target = &r - break - } - } + target := findRouter(cfg, routerName) if target == nil { log.Fatalf("no router named %q in config", routerName) } @@ -324,6 +328,130 @@ func runAPI(cfg *Config, routerName string, apiArgs []string) { } } +// runSchema introspects a MikroTik API path on a single router and shows +// what parameter names exist in the wild. ROS7 keeps shuffling property +// names between point releases (address-families → afi, type=blackhole +// → blackhole flag, etc.), and the live router + the .rsc exports are +// the only authoritative reference. Two sources, in order: +// +// 1. Live API: union of property keys observed across all rows under +// /print, with one example value per key. Falls back to /get +// for singletons (/system/identity, /ip/dns, etc.). +// +// 2. Local .rsc exports: any block whose first line is the matching +// ROS-script path (e.g. `/routing bgp connection` for input +// `/routing/bgp/connection`), with the next several non-blank lines +// so the user can see actual `add ...` invocations. +// +// Trailing /add /set /remove /print /get on the input path are stripped +// — typing `mikrotik-tool schema verona /routing/bgp/connection/add` is +// the natural reflex when you've just hit "unknown parameter" on an add. +func runSchema(cfg *Config, routerName, path string) { + target := findRouter(cfg, routerName) + if target == nil { + log.Fatalf("no router named %q in config", routerName) + } + r := cfg.resolve(*target) + + norm := strings.TrimPrefix(path, "/") + norm = strings.Trim(norm, "/") + for _, suffix := range []string{"/add", "/set", "/remove", "/print", "/get", "/getall"} { + norm = strings.TrimSuffix(norm, suffix) + } + apiPath := "/" + norm + + fmt.Printf("=== schema: %s on %s ===\n", apiPath, r.Name) + + c, err := dialAPI(r) + if err != nil { + log.Fatalf("[%s] %v", r.Name, err) + } + defer c.Close() + + keys := map[string]string{} + source := "" + + if rep, err := c.RunArgs([]string{apiPath + "/print"}); err == nil && len(rep.Re) > 0 { + source = fmt.Sprintf("union of keys across %d existing item(s)", len(rep.Re)) + for _, row := range rep.Re { + for k, v := range row.Map { + if _, exists := keys[k]; !exists { + keys[k] = v + } + } + } + } else if rep, err := c.RunArgs([]string{apiPath + "/get"}); err == nil && len(rep.Re) > 0 { + source = "singleton (/get)" + for _, row := range rep.Re { + for k, v := range row.Map { + keys[k] = v + } + } + } + + if len(keys) == 0 { + fmt.Printf("\n# live api: no rows visible at %s\n", apiPath) + } else { + fmt.Printf("\n# live api: %s\n", source) + var names []string + for k := range keys { + names = append(names, k) + } + sort.Strings(names) + for _, k := range names { + example := keys[k] + if len(example) > 60 { + example = example[:60] + "…" + } + fmt.Printf(" %-32s e.g. %s\n", k, example) + } + } + + rscPath := "/" + strings.ReplaceAll(norm, "/", " ") + fmt.Printf("\n# .rsc examples (matching `%s`):\n", rscPath) + rscFiles, _ := filepath.Glob("*.rsc") + sort.Strings(rscFiles) + found := 0 + const maxExamples = 6 + const maxLinesPerExample = 12 + for _, f := range rscFiles { + if found >= maxExamples { + break + } + data, err := os.ReadFile(f) + if err != nil { + continue + } + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if found >= maxExamples { + break + } + if strings.TrimSpace(line) != rscPath { + continue + } + fmt.Printf("\n [%s:%d]\n", f, i+1) + for j := 1; j < maxLinesPerExample && i+j < len(lines); j++ { + next := lines[i+j] + trimmed := strings.TrimSpace(next) + if strings.HasPrefix(trimmed, "/") { + break + } + if trimmed == "" { + break + } + fmt.Printf(" %s\n", strings.TrimRight(next, "\r")) + } + found++ + } + } + if found == 0 { + fmt.Println(" (no matches in local .rsc files)") + } else if found >= maxExamples { + fmt.Printf("\n (truncated at %d examples)\n", maxExamples) + } +} + func runList(cfg *Config) { for _, r := range cfg.Routers { r2 := cfg.resolve(r) @@ -339,7 +467,12 @@ commands: export ssh to each router, run "/export", save stdout to .rsc locally inventory discover access points, backhaul radios, and tower switches across every router (skipping customer equipment) and re-render - inventory.yaml in place`) + inventory.yaml in place + api run an arbitrary API command on one router + (e.g. mikrotik-tool api verona /ip/route/print) + schema show parameter names accepted under an API path on one router, + plus matching examples from local .rsc exports + (e.g. mikrotik-tool schema verona /routing/bgp/connection)`) os.Exit(2) } @@ -388,6 +521,12 @@ func main() { os.Exit(2) } runAPI(cfg, args[1], args[2:]) + case "schema": + if len(args) < 3 { + fmt.Fprintln(os.Stderr, "usage: mikrotik-tool schema ") + os.Exit(2) + } + runSchema(cfg, args[1], args[2]) default: fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) usage() diff --git a/mikrotik-tool/mikrotik-tool b/mikrotik-tool/mikrotik-tool index c39a9e8..03f8758 100755 Binary files a/mikrotik-tool/mikrotik-tool and b/mikrotik-tool/mikrotik-tool differ diff --git a/mikrotik-tool/newhope.rsc b/mikrotik-tool/newhope.rsc index 3eafc55..8d29c4f 100644 --- a/mikrotik-tool/newhope.rsc +++ b/mikrotik-tool/newhope.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = 5HTF-YFWV # # model = CCR1009-7G-1C-1S+ diff --git a/mikrotik-tool/verona-v6-apply.rsc b/mikrotik-tool/verona-v6-apply.rsc index 4ff60d0..109eb80 100644 --- a/mikrotik-tool/verona-v6-apply.rsc +++ b/mikrotik-tool/verona-v6-apply.rsc @@ -118,20 +118,22 @@ add chain=forward action=drop # ----------------------------------------------------------------------------- # APPLY — iBGP to edge over v4 transport, carrying v6 NLRI # ----------------------------------------------------------------------------- +# ROS 7.21.4 syntax notes (verified live on verona 2026-05-09): +# - parameter is `afi=ip,ipv6` NOT `address-families=...` +# - need a `/routing bgp instance` to reference (carries the local AS) +# - `templates=default` ties it to the existing /routing/bgp/template # Output filter: only advertise verona+altoga /44 aggregates over this session. /routing filter rule add chain=v6-ibgp-out disabled=no rule="if (dst==2606:1c80:1000::/44 || dst==2606:1c80:1500::/44) { accept; } else { reject; }" +/routing bgp instance +add name=bgp-instance-1 as=393837 router-id=10.254.254.101 + /routing bgp connection -add name=ibgp-edge-v6 \ - remote.address=10.254.254.254 \ - remote.as=393837 \ - local.address=10.254.254.101 \ - local.role=ibgp \ - address-families=ipv6 \ - multihop=yes \ - output.filter-chain=v6-ibgp-out \ - nexthop-choice=force-self \ +add name=ibgp-edge-v6 instance=bgp-instance-1 templates=default \ + afi=ip,ipv6 local.role=ibgp local.address=10.254.254.101 \ + remote.address=10.254.254.254 remote.as=393837 multihop=yes \ + nexthop-choice=force-self output.filter-chain=v6-ibgp-out \ comment="iBGP to edge, v4 transport / v6 NLRI" # ----------------------------------------------------------------------------- diff --git a/mikrotik-tool/verona.rsc b/mikrotik-tool/verona.rsc index 249a9de..7ee997e 100644 --- a/mikrotik-tool/verona.rsc +++ b/mikrotik-tool/verona.rsc @@ -1,4 +1,4 @@ -# 2026-05-09 08:51:42 by RouterOS 7.21.4 +# 2026-05-09 09:00:22 by RouterOS 7.21.4 # software id = Y1CT-1WB1 # # model = CCR2004-16G-2S+ @@ -425,6 +425,8 @@ set ether15_wave_n queue=FQ_Codel set ether16 queue=FQ_Codel set sfp-sfpplus1-verona-tower-switch queue=FQ_Codel set sfp-sfpplus2-switch queue=FQ_Codel +/routing bgp instance +add as=393837 name=bgp-instance-1 router-id=10.254.254.101 /routing bgp template set default disabled=no output.network=bgp-networks /routing id @@ -974,6 +976,11 @@ add address=204.110.191.2 disabled=yes require-message-auth=no service=\ set accept=yes /routing bfd configuration add disabled=no interfaces=all min-rx=200ms min-tx=200ms multiplier=5 +/routing bgp connection +add afi=ip,ipv6 comment="iBGP to edge, v4 transport / v6 NLRI" instance=\ + bgp-instance-1 local.address=10.254.254.101 .role=ibgp multihop=yes name=\ + ibgp-edge-v6 nexthop-choice=force-self output.filter-chain=v6-ibgp-out \ + remote.address=10.254.254.254 .as=393837 templates=default /routing filter rule add chain=ospf-in disabled=no rule="accept;" add chain=ospf-out disabled=no rule="accept;"