|
|
@@ -0,0 +1,801 @@
|
|
|
+package cli
|
|
|
+
|
|
|
+import (
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+// TerminalWriter is an interface satisfied by *terminal.Terminal and mock terminals.
|
|
|
+type TerminalWriter interface {
|
|
|
+ Write(buf []byte) (n int, err error)
|
|
|
+}
|
|
|
+
|
|
|
+// Node represents a command or subsection in the CLI hierarchy.
|
|
|
+type Node struct {
|
|
|
+ Name string
|
|
|
+ Aliases []string
|
|
|
+ Description string
|
|
|
+ Children []*Node
|
|
|
+ Dynamic func() []string
|
|
|
+}
|
|
|
+
|
|
|
+// AddChild appends a child node to this node and returns the child.
|
|
|
+func (n *Node) AddChild(child *Node) *Node {
|
|
|
+ n.Children = append(n.Children, child)
|
|
|
+ return child
|
|
|
+}
|
|
|
+
|
|
|
+// FindChild searches for a child matching token by Name, Aliases, or unique prefix.
|
|
|
+func (n *Node) FindChild(token string) *Node {
|
|
|
+ token = strings.ToLower(token)
|
|
|
+ // 1. Exact Name match
|
|
|
+ for _, ch := range n.Children {
|
|
|
+ if strings.ToLower(ch.Name) == token {
|
|
|
+ return ch
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 2. Exact Aliases match
|
|
|
+ for _, ch := range n.Children {
|
|
|
+ for _, a := range ch.Aliases {
|
|
|
+ if strings.ToLower(a) == token {
|
|
|
+ return ch
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 3. Unique prefix match among children (excluding parameters ending in '=')
|
|
|
+ var match *Node
|
|
|
+ for _, ch := range n.Children {
|
|
|
+ if !strings.HasSuffix(ch.Name, "=") && strings.HasPrefix(strings.ToLower(ch.Name), token) {
|
|
|
+ if match != nil {
|
|
|
+ return nil // Ambiguous
|
|
|
+ }
|
|
|
+ match = ch
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return match
|
|
|
+}
|
|
|
+
|
|
|
+// Completer manages tab-completion state and double-tab candidate listing.
|
|
|
+type Completer struct {
|
|
|
+ term TerminalWriter
|
|
|
+ executor *Executor
|
|
|
+ root *Node
|
|
|
+ termWidth int
|
|
|
+ mu sync.Mutex
|
|
|
+ lastTabTime time.Time
|
|
|
+ lastTabLine string
|
|
|
+ lastTabPos int
|
|
|
+}
|
|
|
+
|
|
|
+// NewCompleter creates and initializes a Completer with the full command tree.
|
|
|
+func NewCompleter(term TerminalWriter, executor *Executor) *Completer {
|
|
|
+ c := &Completer{
|
|
|
+ term: term,
|
|
|
+ executor: executor,
|
|
|
+ termWidth: 80,
|
|
|
+ }
|
|
|
+ c.root = c.buildCommandTree()
|
|
|
+ return c
|
|
|
+}
|
|
|
+
|
|
|
+// SetWidth sets the terminal width for candidate column formatting.
|
|
|
+func (c *Completer) SetWidth(w int) {
|
|
|
+ c.mu.Lock()
|
|
|
+ defer c.mu.Unlock()
|
|
|
+ if w > 20 {
|
|
|
+ c.termWidth = w
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Callback is the AutoCompleteCallback for golang.org/x/term.Terminal.
|
|
|
+func (c *Completer) Callback(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
|
|
+ if key != '\t' {
|
|
|
+ return "", 0, false
|
|
|
+ }
|
|
|
+
|
|
|
+ c.mu.Lock()
|
|
|
+ defer c.mu.Unlock()
|
|
|
+
|
|
|
+ candidates, prefixBeforeWord, currentWord, isParamVal, paramPrefix := c.getCompletions(line, pos)
|
|
|
+ if len(candidates) == 0 {
|
|
|
+ // Nothing to complete
|
|
|
+ c.lastTabTime = time.Time{}
|
|
|
+ c.lastTabLine = ""
|
|
|
+ c.lastTabPos = 0
|
|
|
+ return "", 0, false
|
|
|
+ }
|
|
|
+
|
|
|
+ suffix := ""
|
|
|
+ if pos < len(line) {
|
|
|
+ suffix = line[pos:]
|
|
|
+ }
|
|
|
+
|
|
|
+ // Single match: complete it immediately!
|
|
|
+ if len(candidates) == 1 {
|
|
|
+ c.lastTabTime = time.Time{}
|
|
|
+ c.lastTabLine = ""
|
|
|
+ c.lastTabPos = 0
|
|
|
+
|
|
|
+ match := candidates[0]
|
|
|
+ completedToken := match
|
|
|
+ if isParamVal {
|
|
|
+ completedToken = paramPrefix + match + " "
|
|
|
+ } else if strings.HasSuffix(match, "=") {
|
|
|
+ // Parameter key, e.g. "address=" -> no trailing space
|
|
|
+ completedToken = match
|
|
|
+ } else {
|
|
|
+ completedToken = match + " "
|
|
|
+ }
|
|
|
+
|
|
|
+ newLine = prefixBeforeWord + completedToken + suffix
|
|
|
+ newPos = len(prefixBeforeWord) + len(completedToken)
|
|
|
+ return newLine, newPos, true
|
|
|
+ }
|
|
|
+
|
|
|
+ // Multiple candidates: find Longest Common Prefix (LCP)
|
|
|
+ lcp := longestCommonPrefix(candidates)
|
|
|
+ targetWord := currentWord
|
|
|
+ if isParamVal {
|
|
|
+ targetWord = currentWord[len(paramPrefix):]
|
|
|
+ }
|
|
|
+
|
|
|
+ // If LCP can extend the current input, expand it
|
|
|
+ if len(lcp) > len(targetWord) {
|
|
|
+ extendedToken := lcp
|
|
|
+ if isParamVal {
|
|
|
+ extendedToken = paramPrefix + lcp
|
|
|
+ }
|
|
|
+ newLine = prefixBeforeWord + extendedToken + suffix
|
|
|
+ newPos = len(prefixBeforeWord) + len(extendedToken)
|
|
|
+
|
|
|
+ // Record tab state so an immediate second tab shows remaining options
|
|
|
+ c.lastTabTime = time.Now()
|
|
|
+ c.lastTabLine = newLine
|
|
|
+ c.lastTabPos = newPos
|
|
|
+ return newLine, newPos, true
|
|
|
+ }
|
|
|
+
|
|
|
+ // LCP cannot extend input further: check for 2nd Tab (within 1000ms)
|
|
|
+ isSecondTab := time.Since(c.lastTabTime) <= 1000*time.Millisecond &&
|
|
|
+ c.lastTabLine == line &&
|
|
|
+ c.lastTabPos == pos
|
|
|
+
|
|
|
+ if !isSecondTab {
|
|
|
+ // 1st Tab: Record state and wait for potential 2nd Tab
|
|
|
+ c.lastTabTime = time.Now()
|
|
|
+ c.lastTabLine = line
|
|
|
+ c.lastTabPos = pos
|
|
|
+ return line, pos, true
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2nd Tab: DISPLAY ALL CANDIDATES!
|
|
|
+ c.lastTabTime = time.Time{}
|
|
|
+ c.lastTabLine = ""
|
|
|
+ c.lastTabPos = 0
|
|
|
+
|
|
|
+ if c.term != nil {
|
|
|
+ formatted := c.formatCandidates(candidates)
|
|
|
+ _, _ = c.term.Write([]byte(formatted))
|
|
|
+ }
|
|
|
+
|
|
|
+ return line, pos, true
|
|
|
+}
|
|
|
+
|
|
|
+// getCompletions computes the candidate completions for the current cursor context.
|
|
|
+func (c *Completer) getCompletions(line string, pos int) (candidates []string, prefixBeforeWord string, currentWord string, isParamVal bool, paramPrefix string) {
|
|
|
+ if pos > len(line) {
|
|
|
+ pos = len(line)
|
|
|
+ }
|
|
|
+ input := line[:pos]
|
|
|
+
|
|
|
+ lastSpace := strings.LastIndexAny(input, " \t")
|
|
|
+ if lastSpace == -1 {
|
|
|
+ // Single word so far (or empty)
|
|
|
+ if strings.HasPrefix(input, "/") {
|
|
|
+ prefixBeforeWord = "/"
|
|
|
+ currentWord = input[1:]
|
|
|
+ } else {
|
|
|
+ prefixBeforeWord = ""
|
|
|
+ currentWord = input
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ prefixBeforeWord = input[:lastSpace+1]
|
|
|
+ currentWord = input[lastSpace+1:]
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse preceding words to navigate tree
|
|
|
+ rawPrefix := strings.TrimSpace(prefixBeforeWord)
|
|
|
+ hasSlash := strings.HasPrefix(rawPrefix, "/")
|
|
|
+ if hasSlash {
|
|
|
+ rawPrefix = strings.TrimPrefix(rawPrefix, "/")
|
|
|
+ }
|
|
|
+
|
|
|
+ var precedingTokens []string
|
|
|
+ if rawPrefix != "" {
|
|
|
+ precedingTokens = strings.Fields(rawPrefix)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Traverse tree to find current node and track used parameter keys
|
|
|
+ currNode := c.root
|
|
|
+ usedParams := make(map[string]bool)
|
|
|
+
|
|
|
+ for _, token := range precedingTokens {
|
|
|
+ if idx := strings.Index(token, "="); idx != -1 {
|
|
|
+ // Parameter key=value, stay at current node
|
|
|
+ key := strings.ToLower(token[:idx+1])
|
|
|
+ usedParams[key] = true
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ next := currNode.FindChild(token)
|
|
|
+ if next != nil {
|
|
|
+ currNode = next
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if user is typing a parameter value (e.g. "interface=eth" or "role=")
|
|
|
+ if idx := strings.Index(currentWord, "="); idx != -1 {
|
|
|
+ paramKey := strings.ToLower(currentWord[:idx+1])
|
|
|
+ paramVal := strings.ToLower(currentWord[idx+1:])
|
|
|
+ paramChild := currNode.FindChild(paramKey)
|
|
|
+ if paramChild != nil && paramChild.Dynamic != nil {
|
|
|
+ isParamVal = true
|
|
|
+ paramPrefix = currentWord[:idx+1]
|
|
|
+ for _, val := range paramChild.Dynamic() {
|
|
|
+ if strings.HasPrefix(strings.ToLower(val), paramVal) {
|
|
|
+ candidates = append(candidates, val)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sort.Strings(candidates)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Gather child commands and parameters
|
|
|
+ search := strings.ToLower(currentWord)
|
|
|
+ for _, ch := range currNode.Children {
|
|
|
+ nameLower := strings.ToLower(ch.Name)
|
|
|
+ if strings.HasSuffix(nameLower, "=") {
|
|
|
+ if usedParams[nameLower] {
|
|
|
+ continue // parameter already provided
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(nameLower, search) {
|
|
|
+ candidates = append(candidates, ch.Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if current node has dynamic positional choices (e.g. interface names or service names)
|
|
|
+ if currNode.Dynamic != nil {
|
|
|
+ for _, dyn := range currNode.Dynamic() {
|
|
|
+ if strings.HasPrefix(strings.ToLower(dyn), search) {
|
|
|
+ candidates = append(candidates, dyn)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Deduplicate and sort
|
|
|
+ candidates = uniqueStrings(candidates)
|
|
|
+ sort.Strings(candidates)
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+func (c *Completer) formatCandidates(candidates []string) string {
|
|
|
+ if len(candidates) == 0 {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ termWidth := c.termWidth
|
|
|
+ if termWidth < 40 {
|
|
|
+ termWidth = 80
|
|
|
+ }
|
|
|
+
|
|
|
+ maxLen := 0
|
|
|
+ for _, cand := range candidates {
|
|
|
+ if len(cand) > maxLen {
|
|
|
+ maxLen = len(cand)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ colWidth := maxLen + 3
|
|
|
+ if colWidth < 10 {
|
|
|
+ colWidth = 10
|
|
|
+ }
|
|
|
+
|
|
|
+ numCols := (termWidth - 4) / colWidth
|
|
|
+ if numCols < 1 {
|
|
|
+ numCols = 1
|
|
|
+ }
|
|
|
+ if numCols > len(candidates) {
|
|
|
+ numCols = len(candidates)
|
|
|
+ }
|
|
|
+
|
|
|
+ numRows := (len(candidates) + numCols - 1) / numCols
|
|
|
+
|
|
|
+ var sb strings.Builder
|
|
|
+ sb.WriteString("\n")
|
|
|
+
|
|
|
+ for r := 0; r < numRows; r++ {
|
|
|
+ sb.WriteString(" ")
|
|
|
+ for col := 0; col < numCols; col++ {
|
|
|
+ idx := col*numRows + r
|
|
|
+ if idx < len(candidates) {
|
|
|
+ cand := candidates[idx]
|
|
|
+ colored := colorizeCandidate(cand)
|
|
|
+ sb.WriteString(colored)
|
|
|
+ if col < numCols-1 && (col+1)*numRows+r < len(candidates) {
|
|
|
+ pad := colWidth - len(cand)
|
|
|
+ if pad > 0 {
|
|
|
+ sb.WriteString(strings.Repeat(" ", pad))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sb.WriteString("\n")
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.String()
|
|
|
+}
|
|
|
+
|
|
|
+func colorizeCandidate(s string) string {
|
|
|
+ if strings.HasSuffix(s, "=") {
|
|
|
+ return ColorYellow + s + ColorReset
|
|
|
+ }
|
|
|
+ switch s {
|
|
|
+ case "print", "add", "remove", "enable", "disable", "set", "reboot", "flush", "renew", "release", "connect", "disconnect":
|
|
|
+ return ColorGreen + s + ColorReset
|
|
|
+ case "interface", "ip", "routing", "tool", "log", "user", "system",
|
|
|
+ "vlan", "bridge", "wireguard", "eoip", "vrrp", "pppoe-client",
|
|
|
+ "address", "route", "dhcp-client", "dhcp-server", "dns", "firewall", "service",
|
|
|
+ "filter", "nat", "mangle", "address-list", "bgp", "ospf", "port", "peers", "lease", "network", "cache", "static", "rule", "table", "instance", "area", "neighbor", "resource", "identity":
|
|
|
+ return ColorCyan + s + ColorReset
|
|
|
+ case "api", "api-ssl", "ftp", "ssh", "telnet", "web", "web-ssl":
|
|
|
+ return ColorMagenta + s + ColorReset
|
|
|
+ default:
|
|
|
+ return ColorWhite + s + ColorReset
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func longestCommonPrefix(strs []string) string {
|
|
|
+ if len(strs) == 0 {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ prefix := strs[0]
|
|
|
+ for _, s := range strs[1:] {
|
|
|
+ for !strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
|
|
|
+ if len(prefix) == 0 {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ prefix = prefix[:len(prefix)-1]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return prefix
|
|
|
+}
|
|
|
+
|
|
|
+func uniqueStrings(slice []string) []string {
|
|
|
+ seen := make(map[string]bool)
|
|
|
+ var result []string
|
|
|
+ for _, s := range slice {
|
|
|
+ if !seen[s] {
|
|
|
+ seen[s] = true
|
|
|
+ result = append(result, s)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// buildCommandTree creates the full tree of commands, subsections, actions, parameters, and dynamic providers.
|
|
|
+func (c *Completer) buildCommandTree() *Node {
|
|
|
+ root := &Node{Name: "root"}
|
|
|
+
|
|
|
+ // Helper for dynamic interfaces
|
|
|
+ dynInterfaces := func() []string {
|
|
|
+ if c.executor != nil && c.executor.netMgr != nil {
|
|
|
+ ifaces, err := c.executor.netMgr.ListInterfaces()
|
|
|
+ if err == nil {
|
|
|
+ var names []string
|
|
|
+ for _, iface := range ifaces {
|
|
|
+ names = append(names, iface.Name)
|
|
|
+ }
|
|
|
+ return names
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return []string{"eth0", "eth1", "lo"}
|
|
|
+ }
|
|
|
+
|
|
|
+ // Helper for services
|
|
|
+ dynServices := func() []string {
|
|
|
+ return []string{"api", "api-ssl", "ftp", "ssh", "telnet", "web", "web-ssl"}
|
|
|
+ }
|
|
|
+
|
|
|
+ // Common standard actions
|
|
|
+ addPrint := func(n *Node) {
|
|
|
+ n.AddChild(&Node{Name: "print", Aliases: []string{"pr", "show", "list"}})
|
|
|
+ }
|
|
|
+ addRemove := func(n *Node) {
|
|
|
+ n.AddChild(&Node{Name: "remove", Aliases: []string{"rem", "delete"}})
|
|
|
+ }
|
|
|
+ addEnableDisable := func(n *Node) {
|
|
|
+ n.AddChild(&Node{Name: "enable"})
|
|
|
+ n.AddChild(&Node{Name: "disable"})
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. /interface
|
|
|
+ ifaceNode := root.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
|
|
|
+ addPrint(ifaceNode)
|
|
|
+ ifaceNode.AddChild(&Node{Name: "enable", Dynamic: dynInterfaces})
|
|
|
+ ifaceNode.AddChild(&Node{Name: "disable", Dynamic: dynInterfaces})
|
|
|
+ ifaceSet := ifaceNode.AddChild(&Node{Name: "set", Dynamic: dynInterfaces})
|
|
|
+ ifaceSet.AddChild(&Node{Name: "mtu="})
|
|
|
+
|
|
|
+ // /interface vlan
|
|
|
+ vlanNode := ifaceNode.AddChild(&Node{Name: "vlan", Aliases: []string{"vlans"}})
|
|
|
+ addPrint(vlanNode)
|
|
|
+ addRemove(vlanNode)
|
|
|
+ vlanAdd := vlanNode.AddChild(&Node{Name: "add"})
|
|
|
+ vlanAdd.AddChild(&Node{Name: "name="})
|
|
|
+ vlanAdd.AddChild(&Node{Name: "vlan-id="})
|
|
|
+ vlanAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+
|
|
|
+ // /interface bridge
|
|
|
+ bridgeNode := ifaceNode.AddChild(&Node{Name: "bridge", Aliases: []string{"bridges"}})
|
|
|
+ addPrint(bridgeNode)
|
|
|
+ addRemove(bridgeNode)
|
|
|
+ addEnableDisable(bridgeNode)
|
|
|
+ bridgeAdd := bridgeNode.AddChild(&Node{Name: "add"})
|
|
|
+ bridgeAdd.AddChild(&Node{Name: "name="})
|
|
|
+
|
|
|
+ // /interface bridge port
|
|
|
+ bridgePortNode := bridgeNode.AddChild(&Node{Name: "port", Aliases: []string{"ports"}})
|
|
|
+ addPrint(bridgePortNode)
|
|
|
+ addRemove(bridgePortNode)
|
|
|
+ addEnableDisable(bridgePortNode)
|
|
|
+ bridgePortAdd := bridgePortNode.AddChild(&Node{Name: "add"})
|
|
|
+ bridgePortAdd.AddChild(&Node{Name: "bridge="})
|
|
|
+ bridgePortAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ bridgePortAdd.AddChild(&Node{Name: "pvid="})
|
|
|
+
|
|
|
+ // /interface wireguard
|
|
|
+ wgNode := ifaceNode.AddChild(&Node{Name: "wireguard", Aliases: []string{"wg"}})
|
|
|
+ addPrint(wgNode)
|
|
|
+ addRemove(wgNode)
|
|
|
+ addEnableDisable(wgNode)
|
|
|
+ wgAdd := wgNode.AddChild(&Node{Name: "add"})
|
|
|
+ wgAdd.AddChild(&Node{Name: "name="})
|
|
|
+ wgAdd.AddChild(&Node{Name: "listen-port="})
|
|
|
+ wgAdd.AddChild(&Node{Name: "private-key="})
|
|
|
+
|
|
|
+ // /interface wireguard peers
|
|
|
+ wgPeers := wgNode.AddChild(&Node{Name: "peers", Aliases: []string{"peer"}})
|
|
|
+ addPrint(wgPeers)
|
|
|
+ addRemove(wgPeers)
|
|
|
+ wgPeersAdd := wgPeers.AddChild(&Node{Name: "add"})
|
|
|
+ wgPeersAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ wgPeersAdd.AddChild(&Node{Name: "public-key="})
|
|
|
+ wgPeersAdd.AddChild(&Node{Name: "allowed-address="})
|
|
|
+ wgPeersAdd.AddChild(&Node{Name: "endpoint-address="})
|
|
|
+ wgPeersAdd.AddChild(&Node{Name: "endpoint-port="})
|
|
|
+
|
|
|
+ // /interface eoip
|
|
|
+ eoipNode := ifaceNode.AddChild(&Node{Name: "eoip"})
|
|
|
+ addPrint(eoipNode)
|
|
|
+ addRemove(eoipNode)
|
|
|
+ addEnableDisable(eoipNode)
|
|
|
+ eoipAdd := eoipNode.AddChild(&Node{Name: "add"})
|
|
|
+ eoipAdd.AddChild(&Node{Name: "name="})
|
|
|
+ eoipAdd.AddChild(&Node{Name: "remote-ip="})
|
|
|
+ eoipAdd.AddChild(&Node{Name: "tunnel-id="})
|
|
|
+
|
|
|
+ // /interface vrrp
|
|
|
+ vrrpNode := ifaceNode.AddChild(&Node{Name: "vrrp"})
|
|
|
+ addPrint(vrrpNode)
|
|
|
+ addRemove(vrrpNode)
|
|
|
+ addEnableDisable(vrrpNode)
|
|
|
+ vrrpAdd := vrrpNode.AddChild(&Node{Name: "add"})
|
|
|
+ vrrpAdd.AddChild(&Node{Name: "name="})
|
|
|
+ vrrpAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ vrrpAdd.AddChild(&Node{Name: "vrid="})
|
|
|
+ vrrpAdd.AddChild(&Node{Name: "priority="})
|
|
|
+
|
|
|
+ // /interface pppoe-client
|
|
|
+ pppoeNode := ifaceNode.AddChild(&Node{Name: "pppoe-client"})
|
|
|
+ addPrint(pppoeNode)
|
|
|
+ addRemove(pppoeNode)
|
|
|
+ pppoeNode.AddChild(&Node{Name: "connect"})
|
|
|
+ pppoeNode.AddChild(&Node{Name: "disconnect"})
|
|
|
+ pppoeAdd := pppoeNode.AddChild(&Node{Name: "add"})
|
|
|
+ pppoeAdd.AddChild(&Node{Name: "name="})
|
|
|
+ pppoeAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ pppoeAdd.AddChild(&Node{Name: "user="})
|
|
|
+ pppoeAdd.AddChild(&Node{Name: "password="})
|
|
|
+
|
|
|
+ // 2. /ip
|
|
|
+ ipNode := root.AddChild(&Node{Name: "ip"})
|
|
|
+
|
|
|
+ // /ip address
|
|
|
+ ipAddrNode := ipNode.AddChild(&Node{Name: "address", Aliases: []string{"addr"}})
|
|
|
+ addPrint(ipAddrNode)
|
|
|
+ addRemove(ipAddrNode)
|
|
|
+ ipAddrAdd := ipAddrNode.AddChild(&Node{Name: "add"})
|
|
|
+ ipAddrAdd.AddChild(&Node{Name: "address="})
|
|
|
+ ipAddrAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ ipAddrAdd.AddChild(&Node{Name: "network="})
|
|
|
+
|
|
|
+ // /ip route
|
|
|
+ ipRouteNode := ipNode.AddChild(&Node{Name: "route", Aliases: []string{"routes"}})
|
|
|
+ addPrint(ipRouteNode)
|
|
|
+ addRemove(ipRouteNode)
|
|
|
+ addEnableDisable(ipRouteNode)
|
|
|
+ ipRouteAdd := ipRouteNode.AddChild(&Node{Name: "add"})
|
|
|
+ ipRouteAdd.AddChild(&Node{Name: "dst-address="})
|
|
|
+ ipRouteAdd.AddChild(&Node{Name: "gateway="})
|
|
|
+ ipRouteAdd.AddChild(&Node{Name: "distance="})
|
|
|
+
|
|
|
+ // /ip route rule
|
|
|
+ ipRouteRule := ipRouteNode.AddChild(&Node{Name: "rule", Aliases: []string{"rules"}})
|
|
|
+ addPrint(ipRouteRule)
|
|
|
+ addRemove(ipRouteRule)
|
|
|
+ ipRouteRuleAdd := ipRouteRule.AddChild(&Node{Name: "add"})
|
|
|
+ ipRouteRuleAdd.AddChild(&Node{Name: "src-address="})
|
|
|
+ ipRouteRuleAdd.AddChild(&Node{Name: "dst-address="})
|
|
|
+ ipRouteRuleAdd.AddChild(&Node{Name: "table="})
|
|
|
+ ipRouteRuleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"lookup", "unreachable", "drop"} }})
|
|
|
+
|
|
|
+ // /ip route table
|
|
|
+ ipRouteTable := ipRouteNode.AddChild(&Node{Name: "table", Aliases: []string{"tables"}})
|
|
|
+ addPrint(ipRouteTable)
|
|
|
+ addRemove(ipRouteTable)
|
|
|
+ ipRouteTableAdd := ipRouteTable.AddChild(&Node{Name: "add"})
|
|
|
+ ipRouteTableAdd.AddChild(&Node{Name: "name="})
|
|
|
+ ipRouteTableAdd.AddChild(&Node{Name: "fib="})
|
|
|
+
|
|
|
+ // /ip dhcp-client
|
|
|
+ dhcpCliNode := ipNode.AddChild(&Node{Name: "dhcp-client"})
|
|
|
+ addPrint(dhcpCliNode)
|
|
|
+ addRemove(dhcpCliNode)
|
|
|
+ dhcpCliNode.AddChild(&Node{Name: "renew"})
|
|
|
+ dhcpCliNode.AddChild(&Node{Name: "release"})
|
|
|
+ dhcpCliAdd := dhcpCliNode.AddChild(&Node{Name: "add"})
|
|
|
+ dhcpCliAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ dhcpCliAdd.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ // /ip dhcp-server
|
|
|
+ dhcpSrvNode := ipNode.AddChild(&Node{Name: "dhcp-server"})
|
|
|
+ addPrint(dhcpSrvNode)
|
|
|
+ addRemove(dhcpSrvNode)
|
|
|
+ addEnableDisable(dhcpSrvNode)
|
|
|
+ dhcpSrvAdd := dhcpSrvNode.AddChild(&Node{Name: "add"})
|
|
|
+ dhcpSrvAdd.AddChild(&Node{Name: "name="})
|
|
|
+ dhcpSrvAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ dhcpSrvAdd.AddChild(&Node{Name: "address-pool="})
|
|
|
+ dhcpSrvAdd.AddChild(&Node{Name: "lease-time="})
|
|
|
+
|
|
|
+ // /ip dhcp-server lease
|
|
|
+ dhcpLeaseNode := dhcpSrvNode.AddChild(&Node{Name: "lease", Aliases: []string{"leases"}})
|
|
|
+ addPrint(dhcpLeaseNode)
|
|
|
+ addRemove(dhcpLeaseNode)
|
|
|
+
|
|
|
+ // /ip dhcp-server network
|
|
|
+ dhcpNetNode := dhcpSrvNode.AddChild(&Node{Name: "network", Aliases: []string{"networks"}})
|
|
|
+ addPrint(dhcpNetNode)
|
|
|
+ addRemove(dhcpNetNode)
|
|
|
+ dhcpNetAdd := dhcpNetNode.AddChild(&Node{Name: "add"})
|
|
|
+ dhcpNetAdd.AddChild(&Node{Name: "address="})
|
|
|
+ dhcpNetAdd.AddChild(&Node{Name: "gateway="})
|
|
|
+ dhcpNetAdd.AddChild(&Node{Name: "dns-server="})
|
|
|
+
|
|
|
+ // /ip dns
|
|
|
+ dnsNode := ipNode.AddChild(&Node{Name: "dns"})
|
|
|
+ addPrint(dnsNode)
|
|
|
+ dnsSet := dnsNode.AddChild(&Node{Name: "set"})
|
|
|
+ dnsSet.AddChild(&Node{Name: "servers="})
|
|
|
+ dnsSet.AddChild(&Node{Name: "allow-remote-requests=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ dnsCache := dnsNode.AddChild(&Node{Name: "cache"})
|
|
|
+ addPrint(dnsCache)
|
|
|
+ dnsCache.AddChild(&Node{Name: "flush"})
|
|
|
+
|
|
|
+ dnsStatic := dnsNode.AddChild(&Node{Name: "static"})
|
|
|
+ addPrint(dnsStatic)
|
|
|
+ addRemove(dnsStatic)
|
|
|
+ dnsStaticAdd := dnsStatic.AddChild(&Node{Name: "add"})
|
|
|
+ dnsStaticAdd.AddChild(&Node{Name: "name="})
|
|
|
+ dnsStaticAdd.AddChild(&Node{Name: "address="})
|
|
|
+
|
|
|
+ // /ip firewall
|
|
|
+ fwNode := ipNode.AddChild(&Node{Name: "firewall"})
|
|
|
+
|
|
|
+ // /ip firewall filter
|
|
|
+ fwFilter := fwNode.AddChild(&Node{Name: "filter"})
|
|
|
+ addPrint(fwFilter)
|
|
|
+ addRemove(fwFilter)
|
|
|
+ addEnableDisable(fwFilter)
|
|
|
+ fwFilterAdd := fwFilter.AddChild(&Node{Name: "add"})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"forward", "input", "output"} }})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "drop", "reject"} }})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "src-address="})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "dst-address="})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "dst-port="})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "in-interface=", Dynamic: dynInterfaces})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "out-interface=", Dynamic: dynInterfaces})
|
|
|
+ fwFilterAdd.AddChild(&Node{Name: "comment="})
|
|
|
+
|
|
|
+ // /ip firewall nat
|
|
|
+ fwNat := fwNode.AddChild(&Node{Name: "nat"})
|
|
|
+ addPrint(fwNat)
|
|
|
+ addRemove(fwNat)
|
|
|
+ addEnableDisable(fwNat)
|
|
|
+ fwNatAdd := fwNat.AddChild(&Node{Name: "add"})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"dstnat", "srcnat"} }})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "dst-nat", "masquerade", "redirect", "src-nat"} }})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "src-address="})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "dst-address="})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "to-addresses="})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "to-ports="})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "out-interface=", Dynamic: dynInterfaces})
|
|
|
+ fwNatAdd.AddChild(&Node{Name: "comment="})
|
|
|
+
|
|
|
+ // /ip firewall mangle
|
|
|
+ fwMangle := fwNode.AddChild(&Node{Name: "mangle"})
|
|
|
+ addPrint(fwMangle)
|
|
|
+ addRemove(fwMangle)
|
|
|
+ addEnableDisable(fwMangle)
|
|
|
+ fwMangleAdd := fwMangle.AddChild(&Node{Name: "add"})
|
|
|
+ fwMangleAdd.AddChild(&Node{Name: "chain=", Dynamic: func() []string { return []string{"forward", "input", "output", "postrouting", "prerouting"} }})
|
|
|
+ fwMangleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "mark-packet", "mark-routing"} }})
|
|
|
+ fwMangleAdd.AddChild(&Node{Name: "protocol=", Dynamic: func() []string { return []string{"icmp", "tcp", "udp"} }})
|
|
|
+ fwMangleAdd.AddChild(&Node{Name: "src-address="})
|
|
|
+ fwMangleAdd.AddChild(&Node{Name: "dst-address="})
|
|
|
+
|
|
|
+ // /ip firewall address-list
|
|
|
+ fwAddrList := fwNode.AddChild(&Node{Name: "address-list"})
|
|
|
+ addPrint(fwAddrList)
|
|
|
+ addRemove(fwAddrList)
|
|
|
+ addEnableDisable(fwAddrList)
|
|
|
+ fwAddrListAdd := fwAddrList.AddChild(&Node{Name: "add"})
|
|
|
+ fwAddrListAdd.AddChild(&Node{Name: "list="})
|
|
|
+ fwAddrListAdd.AddChild(&Node{Name: "address="})
|
|
|
+ fwAddrListAdd.AddChild(&Node{Name: "comment="})
|
|
|
+
|
|
|
+ // /ip service
|
|
|
+ serviceNode := ipNode.AddChild(&Node{Name: "service", Aliases: []string{"services"}})
|
|
|
+ addPrint(serviceNode)
|
|
|
+ serviceNode.AddChild(&Node{Name: "enable", Dynamic: dynServices})
|
|
|
+ serviceNode.AddChild(&Node{Name: "disable", Dynamic: dynServices})
|
|
|
+ serviceSet := serviceNode.AddChild(&Node{Name: "set", Dynamic: dynServices})
|
|
|
+ serviceSet.AddChild(&Node{Name: "port="})
|
|
|
+ serviceSet.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ // 3. /routing
|
|
|
+ routingNode := root.AddChild(&Node{Name: "routing"})
|
|
|
+
|
|
|
+ // /routing bgp
|
|
|
+ bgpNode := routingNode.AddChild(&Node{Name: "bgp"})
|
|
|
+ bgpInst := bgpNode.AddChild(&Node{Name: "instance"})
|
|
|
+ addPrint(bgpInst)
|
|
|
+ bgpInstSet := bgpInst.AddChild(&Node{Name: "set"})
|
|
|
+ bgpInstSet.AddChild(&Node{Name: "as="})
|
|
|
+ bgpInstSet.AddChild(&Node{Name: "router-id="})
|
|
|
+ bgpInstSet.AddChild(&Node{Name: "enabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ bgpPeer := bgpNode.AddChild(&Node{Name: "peer", Aliases: []string{"peers", "connection"}})
|
|
|
+ addPrint(bgpPeer)
|
|
|
+ addRemove(bgpPeer)
|
|
|
+ addEnableDisable(bgpPeer)
|
|
|
+ bgpPeerAdd := bgpPeer.AddChild(&Node{Name: "add"})
|
|
|
+ bgpPeerAdd.AddChild(&Node{Name: "name="})
|
|
|
+ bgpPeerAdd.AddChild(&Node{Name: "remote-address="})
|
|
|
+ bgpPeerAdd.AddChild(&Node{Name: "remote-as="})
|
|
|
+ bgpPeerAdd.AddChild(&Node{Name: "description="})
|
|
|
+
|
|
|
+ bgpNet := bgpNode.AddChild(&Node{Name: "network"})
|
|
|
+ addPrint(bgpNet)
|
|
|
+ addRemove(bgpNet)
|
|
|
+ bgpNetAdd := bgpNet.AddChild(&Node{Name: "add"})
|
|
|
+ bgpNetAdd.AddChild(&Node{Name: "network="})
|
|
|
+
|
|
|
+ bgpRoutes := bgpNode.AddChild(&Node{Name: "routes"})
|
|
|
+ addPrint(bgpRoutes)
|
|
|
+
|
|
|
+ // /routing ospf
|
|
|
+ ospfNode := routingNode.AddChild(&Node{Name: "ospf"})
|
|
|
+ ospfInst := ospfNode.AddChild(&Node{Name: "instance"})
|
|
|
+ addPrint(ospfInst)
|
|
|
+ ospfInstSet := ospfInst.AddChild(&Node{Name: "set"})
|
|
|
+ ospfInstSet.AddChild(&Node{Name: "router-id="})
|
|
|
+ ospfInstSet.AddChild(&Node{Name: "enabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ ospfArea := ospfNode.AddChild(&Node{Name: "area"})
|
|
|
+ addPrint(ospfArea)
|
|
|
+ addRemove(ospfArea)
|
|
|
+ ospfAreaAdd := ospfArea.AddChild(&Node{Name: "add"})
|
|
|
+ ospfAreaAdd.AddChild(&Node{Name: "name="})
|
|
|
+ ospfAreaAdd.AddChild(&Node{Name: "area-id="})
|
|
|
+
|
|
|
+ ospfIface := ospfNode.AddChild(&Node{Name: "interface"})
|
|
|
+ addPrint(ospfIface)
|
|
|
+ addRemove(ospfIface)
|
|
|
+ ospfIfaceAdd := ospfIface.AddChild(&Node{Name: "add"})
|
|
|
+ ospfIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
|
|
|
+ ospfIfaceAdd.AddChild(&Node{Name: "cost="})
|
|
|
+ ospfIfaceAdd.AddChild(&Node{Name: "priority="})
|
|
|
+ ospfIfaceAdd.AddChild(&Node{Name: "network-type=", Dynamic: func() []string { return []string{"broadcast", "point-to-point"} }})
|
|
|
+
|
|
|
+ ospfNeighbor := ospfNode.AddChild(&Node{Name: "neighbor", Aliases: []string{"neighbors"}})
|
|
|
+ addPrint(ospfNeighbor)
|
|
|
+
|
|
|
+ // /routing filter
|
|
|
+ routingFilter := routingNode.AddChild(&Node{Name: "filter"})
|
|
|
+ addPrint(routingFilter)
|
|
|
+ rfRule := routingFilter.AddChild(&Node{Name: "rule", Aliases: []string{"rules"}})
|
|
|
+ addPrint(rfRule)
|
|
|
+ addRemove(rfRule)
|
|
|
+ rfRuleAdd := rfRule.AddChild(&Node{Name: "add"})
|
|
|
+ rfRuleAdd.AddChild(&Node{Name: "chain="})
|
|
|
+ rfRuleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "discard", "reject"} }})
|
|
|
+ rfRuleAdd.AddChild(&Node{Name: "prefix="})
|
|
|
+ rfRuleAdd.AddChild(&Node{Name: "prefix-len="})
|
|
|
+
|
|
|
+ // 4. /tool
|
|
|
+ toolNode := root.AddChild(&Node{Name: "tool"})
|
|
|
+ toolPing := toolNode.AddChild(&Node{Name: "ping"})
|
|
|
+ toolPing.AddChild(&Node{Name: "count="})
|
|
|
+ toolPing.AddChild(&Node{Name: "size="})
|
|
|
+ toolPing.AddChild(&Node{Name: "interval="})
|
|
|
+
|
|
|
+ toolTrace := toolNode.AddChild(&Node{Name: "traceroute"})
|
|
|
+ toolTrace.AddChild(&Node{Name: "max-hops="})
|
|
|
+
|
|
|
+ toolBtest := toolNode.AddChild(&Node{Name: "bandwidth-test", Aliases: []string{"btest"}})
|
|
|
+ toolBtest.AddChild(&Node{Name: "target="})
|
|
|
+ toolBtest.AddChild(&Node{Name: "duration="})
|
|
|
+ toolBtest.AddChild(&Node{Name: "direction=", Dynamic: func() []string { return []string{"receive", "send", "both"} }})
|
|
|
+
|
|
|
+ // 5. /log
|
|
|
+ logNode := root.AddChild(&Node{Name: "log"})
|
|
|
+ logPrint := logNode.AddChild(&Node{Name: "print", Aliases: []string{"pr", "show"}})
|
|
|
+ logPrint.AddChild(&Node{Name: "limit="})
|
|
|
+ logPrint.AddChild(&Node{Name: "filter="})
|
|
|
+
|
|
|
+ // 6. /user
|
|
|
+ userNode := root.AddChild(&Node{Name: "user", Aliases: []string{"users"}})
|
|
|
+ addPrint(userNode)
|
|
|
+ addRemove(userNode)
|
|
|
+ userAdd := userNode.AddChild(&Node{Name: "add"})
|
|
|
+ userAdd.AddChild(&Node{Name: "name="})
|
|
|
+ userAdd.AddChild(&Node{Name: "password="})
|
|
|
+ userAdd.AddChild(&Node{Name: "role=", Dynamic: func() []string { return []string{"admin", "full", "read", "viewer"} }})
|
|
|
+ userAdd.AddChild(&Node{Name: "comment="})
|
|
|
+
|
|
|
+ userSet := userNode.AddChild(&Node{Name: "set"})
|
|
|
+ userSet.AddChild(&Node{Name: "name="})
|
|
|
+ userSet.AddChild(&Node{Name: "password="})
|
|
|
+ userSet.AddChild(&Node{Name: "role=", Dynamic: func() []string { return []string{"admin", "full", "read", "viewer"} }})
|
|
|
+ userSet.AddChild(&Node{Name: "comment="})
|
|
|
+ userSet.AddChild(&Node{Name: "disabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
|
|
|
+
|
|
|
+ // 7. /system
|
|
|
+ sysNode := root.AddChild(&Node{Name: "system"})
|
|
|
+ sysRes := sysNode.AddChild(&Node{Name: "resource"})
|
|
|
+ addPrint(sysRes)
|
|
|
+
|
|
|
+ sysIdent := sysNode.AddChild(&Node{Name: "identity"})
|
|
|
+ addPrint(sysIdent)
|
|
|
+ sysIdentSet := sysIdent.AddChild(&Node{Name: "set"})
|
|
|
+ sysIdentSet.AddChild(&Node{Name: "name="})
|
|
|
+
|
|
|
+ sysNode.AddChild(&Node{Name: "reboot"})
|
|
|
+
|
|
|
+ // 8. Direct root utility commands
|
|
|
+ root.AddChild(&Node{Name: "clear"})
|
|
|
+ root.AddChild(&Node{Name: "help", Aliases: []string{"?"}})
|
|
|
+ root.AddChild(&Node{Name: "quit"})
|
|
|
+ root.AddChild(&Node{Name: "exit"})
|
|
|
+ root.AddChild(&Node{Name: "ping"})
|
|
|
+ root.AddChild(&Node{Name: "traceroute"})
|
|
|
+
|
|
|
+ return root
|
|
|
+}
|