Implements a Cisco IOS CLI emulator with four modes (user exec, privileged exec, global config, interface config), Cisco-style command abbreviation (e.g. sh run, conf t), enable password flow, and realistic show command output including running-config, interfaces, IP routes, and VLANs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
415 lines
11 KiB
Go
415 lines
11 KiB
Go
package cisco
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// commandResult holds the output of a command and whether the session should end.
|
|
type commandResult struct {
|
|
output string
|
|
exit bool
|
|
}
|
|
|
|
// commandEntry defines a single command with its name and optional sub-commands.
|
|
type commandEntry struct {
|
|
name string
|
|
subs []commandEntry // nil for leaf commands
|
|
}
|
|
|
|
// userExecCommands defines the command tree for user EXEC mode.
|
|
var userExecCommands = []commandEntry{
|
|
{name: "show", subs: []commandEntry{
|
|
{name: "version"},
|
|
{name: "clock"},
|
|
{name: "ip", subs: []commandEntry{
|
|
{name: "route"},
|
|
{name: "interface", subs: []commandEntry{
|
|
{name: "brief"},
|
|
}},
|
|
}},
|
|
{name: "interfaces"},
|
|
{name: "vlan", subs: []commandEntry{
|
|
{name: "brief"},
|
|
}},
|
|
}},
|
|
{name: "enable"},
|
|
{name: "exit"},
|
|
{name: "?"},
|
|
}
|
|
|
|
// privilegedExecCommands extends user commands for privileged mode.
|
|
var privilegedExecCommands = []commandEntry{
|
|
{name: "show", subs: []commandEntry{
|
|
{name: "version"},
|
|
{name: "clock"},
|
|
{name: "ip", subs: []commandEntry{
|
|
{name: "route"},
|
|
{name: "interface", subs: []commandEntry{
|
|
{name: "brief"},
|
|
}},
|
|
}},
|
|
{name: "interfaces"},
|
|
{name: "running-config"},
|
|
{name: "startup-config"},
|
|
{name: "vlan", subs: []commandEntry{
|
|
{name: "brief"},
|
|
}},
|
|
}},
|
|
{name: "configure", subs: []commandEntry{
|
|
{name: "terminal"},
|
|
}},
|
|
{name: "write", subs: []commandEntry{
|
|
{name: "memory"},
|
|
}},
|
|
{name: "copy"},
|
|
{name: "reload"},
|
|
{name: "disable"},
|
|
{name: "terminal", subs: []commandEntry{
|
|
{name: "length"},
|
|
}},
|
|
{name: "exit"},
|
|
{name: "?"},
|
|
}
|
|
|
|
// globalConfigCommands defines the command tree for global config mode.
|
|
var globalConfigCommands = []commandEntry{
|
|
{name: "hostname"},
|
|
{name: "interface"},
|
|
{name: "ip", subs: []commandEntry{
|
|
{name: "route"},
|
|
}},
|
|
{name: "no"},
|
|
{name: "end"},
|
|
{name: "exit"},
|
|
{name: "?"},
|
|
}
|
|
|
|
// interfaceConfigCommands defines the command tree for interface config mode.
|
|
var interfaceConfigCommands = []commandEntry{
|
|
{name: "ip", subs: []commandEntry{
|
|
{name: "address"},
|
|
}},
|
|
{name: "description"},
|
|
{name: "shutdown"},
|
|
{name: "no", subs: []commandEntry{
|
|
{name: "shutdown"},
|
|
}},
|
|
{name: "switchport", subs: []commandEntry{
|
|
{name: "mode"},
|
|
}},
|
|
{name: "end"},
|
|
{name: "exit"},
|
|
{name: "?"},
|
|
}
|
|
|
|
// commandsForMode returns the command tree for the given IOS mode.
|
|
func commandsForMode(mode iosMode) []commandEntry {
|
|
switch mode {
|
|
case modeUserExec:
|
|
return userExecCommands
|
|
case modePrivilegedExec:
|
|
return privilegedExecCommands
|
|
case modeGlobalConfig:
|
|
return globalConfigCommands
|
|
case modeInterfaceConfig:
|
|
return interfaceConfigCommands
|
|
default:
|
|
return userExecCommands
|
|
}
|
|
}
|
|
|
|
// resolveAbbreviation attempts to match an abbreviated word against a list of
|
|
// command entries. It returns the matched entry name, or an error string if
|
|
// ambiguous or unknown.
|
|
func resolveAbbreviation(word string, entries []commandEntry) (string, error) {
|
|
word = strings.ToLower(word)
|
|
var matches []string
|
|
for _, e := range entries {
|
|
if strings.ToLower(e.name) == word {
|
|
return e.name, nil // exact match
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(e.name), word) {
|
|
matches = append(matches, e.name)
|
|
}
|
|
}
|
|
switch len(matches) {
|
|
case 0:
|
|
return "", fmt.Errorf("unknown")
|
|
case 1:
|
|
return matches[0], nil
|
|
default:
|
|
return "", fmt.Errorf("ambiguous")
|
|
}
|
|
}
|
|
|
|
// resolveCommand resolves a sequence of abbreviated words into the canonical
|
|
// command path (e.g., ["sh", "run"] → ["show", "running-config"]).
|
|
// It returns the resolved path, any remaining arguments, and an error if
|
|
// resolution fails.
|
|
func resolveCommand(words []string, entries []commandEntry) ([]string, []string, error) {
|
|
var resolved []string
|
|
current := entries
|
|
|
|
for i, w := range words {
|
|
name, err := resolveAbbreviation(w, current)
|
|
if err != nil {
|
|
if err.Error() == "unknown" && len(resolved) > 0 {
|
|
// Remaining words are arguments to the resolved command.
|
|
return resolved, words[i:], nil
|
|
}
|
|
return resolved, words[i:], err
|
|
}
|
|
resolved = append(resolved, name)
|
|
|
|
// Find sub-commands for the matched entry.
|
|
var nextLevel []commandEntry
|
|
for _, e := range current {
|
|
if e.name == name {
|
|
nextLevel = e.subs
|
|
break
|
|
}
|
|
}
|
|
if nextLevel == nil {
|
|
// Leaf command — rest are arguments.
|
|
return resolved, words[i+1:], nil
|
|
}
|
|
current = nextLevel
|
|
}
|
|
return resolved, nil, nil
|
|
}
|
|
|
|
// dispatch processes a command line in the context of the current IOS state.
|
|
func (s *iosState) dispatch(input string) commandResult {
|
|
words := strings.Fields(input)
|
|
if len(words) == 0 {
|
|
return commandResult{}
|
|
}
|
|
|
|
// Handle "?" as a help request.
|
|
if words[0] == "?" {
|
|
return s.cmdHelp()
|
|
}
|
|
|
|
cmds := commandsForMode(s.mode)
|
|
resolved, args, err := resolveCommand(words, cmds)
|
|
if err != nil {
|
|
if err.Error() == "ambiguous" {
|
|
return commandResult{output: fmt.Sprintf("%% Ambiguous command: \"%s\"", input)}
|
|
}
|
|
return commandResult{output: invalidInput(input)}
|
|
}
|
|
|
|
if len(resolved) == 0 {
|
|
return commandResult{output: invalidInput(input)}
|
|
}
|
|
|
|
cmd := strings.Join(resolved, " ")
|
|
|
|
switch s.mode {
|
|
case modeUserExec:
|
|
return s.dispatchUserExec(cmd, args)
|
|
case modePrivilegedExec:
|
|
return s.dispatchPrivilegedExec(cmd, args)
|
|
case modeGlobalConfig:
|
|
return s.dispatchGlobalConfig(cmd, args)
|
|
case modeInterfaceConfig:
|
|
return s.dispatchInterfaceConfig(cmd, args)
|
|
}
|
|
return commandResult{output: invalidInput(input)}
|
|
}
|
|
|
|
func (s *iosState) dispatchUserExec(cmd string, args []string) commandResult {
|
|
switch cmd {
|
|
case "show version":
|
|
return commandResult{output: showVersion(s)}
|
|
case "show clock":
|
|
return commandResult{output: showClock()}
|
|
case "show ip route":
|
|
return commandResult{output: showIPRoute(s)}
|
|
case "show ip interface brief":
|
|
return commandResult{output: showIPInterfaceBrief(s)}
|
|
case "show interfaces":
|
|
return commandResult{output: showInterfaces(s)}
|
|
case "show vlan brief":
|
|
return commandResult{output: showVLANBrief()}
|
|
case "enable":
|
|
return commandResult{} // handled in Handle() loop
|
|
case "exit":
|
|
return commandResult{exit: true}
|
|
}
|
|
return commandResult{output: invalidInput(cmd)}
|
|
}
|
|
|
|
func (s *iosState) dispatchPrivilegedExec(cmd string, args []string) commandResult {
|
|
switch cmd {
|
|
case "show version":
|
|
return commandResult{output: showVersion(s)}
|
|
case "show clock":
|
|
return commandResult{output: showClock()}
|
|
case "show ip route":
|
|
return commandResult{output: showIPRoute(s)}
|
|
case "show ip interface brief":
|
|
return commandResult{output: showIPInterfaceBrief(s)}
|
|
case "show interfaces":
|
|
return commandResult{output: showInterfaces(s)}
|
|
case "show running-config":
|
|
return commandResult{output: showRunningConfig(s)}
|
|
case "show startup-config":
|
|
return commandResult{output: showRunningConfig(s)} // same as running
|
|
case "show vlan brief":
|
|
return commandResult{output: showVLANBrief()}
|
|
case "configure terminal":
|
|
s.mode = modeGlobalConfig
|
|
return commandResult{output: "Enter configuration commands, one per line. End with CNTL/Z."}
|
|
case "write memory":
|
|
return commandResult{output: "[OK]"}
|
|
case "copy":
|
|
return commandResult{output: "[OK]"}
|
|
case "reload":
|
|
return commandResult{output: "System configuration has been modified. Save? [yes/no]: ", exit: true}
|
|
case "disable":
|
|
s.mode = modeUserExec
|
|
return commandResult{}
|
|
case "terminal length":
|
|
return commandResult{} // accept silently
|
|
case "exit":
|
|
return commandResult{exit: true}
|
|
}
|
|
return commandResult{output: invalidInput(cmd)}
|
|
}
|
|
|
|
func (s *iosState) dispatchGlobalConfig(cmd string, args []string) commandResult {
|
|
switch cmd {
|
|
case "hostname":
|
|
if len(args) < 1 {
|
|
return commandResult{output: "% Incomplete command."}
|
|
}
|
|
s.hostname = args[0]
|
|
return commandResult{}
|
|
case "interface":
|
|
if len(args) < 1 {
|
|
return commandResult{output: "% Incomplete command."}
|
|
}
|
|
ifName := strings.Join(args, "")
|
|
s.currentIf = ifName
|
|
s.mode = modeInterfaceConfig
|
|
return commandResult{}
|
|
case "ip route":
|
|
return commandResult{} // accept silently
|
|
case "no":
|
|
return commandResult{} // accept silently
|
|
case "end":
|
|
s.mode = modePrivilegedExec
|
|
return commandResult{}
|
|
case "exit":
|
|
s.mode = modePrivilegedExec
|
|
return commandResult{}
|
|
}
|
|
return commandResult{output: invalidInput(cmd)}
|
|
}
|
|
|
|
func (s *iosState) dispatchInterfaceConfig(cmd string, args []string) commandResult {
|
|
switch cmd {
|
|
case "ip address":
|
|
if len(args) < 2 {
|
|
return commandResult{output: "% Incomplete command."}
|
|
}
|
|
if iface := s.findInterface(s.currentIf); iface != nil {
|
|
iface.ip = args[0]
|
|
iface.mask = args[1]
|
|
}
|
|
return commandResult{}
|
|
case "description":
|
|
if len(args) < 1 {
|
|
return commandResult{output: "% Incomplete command."}
|
|
}
|
|
if iface := s.findInterface(s.currentIf); iface != nil {
|
|
iface.desc = strings.Join(args, " ")
|
|
}
|
|
return commandResult{}
|
|
case "shutdown":
|
|
if iface := s.findInterface(s.currentIf); iface != nil {
|
|
iface.shutdown = true
|
|
iface.status = "administratively down"
|
|
iface.protocol = "down"
|
|
}
|
|
return commandResult{}
|
|
case "no shutdown":
|
|
if iface := s.findInterface(s.currentIf); iface != nil {
|
|
iface.shutdown = false
|
|
iface.status = "up"
|
|
iface.protocol = "up"
|
|
}
|
|
return commandResult{}
|
|
case "switchport mode":
|
|
return commandResult{} // accept silently
|
|
case "end":
|
|
s.mode = modePrivilegedExec
|
|
s.currentIf = ""
|
|
return commandResult{}
|
|
case "exit":
|
|
s.mode = modeGlobalConfig
|
|
s.currentIf = ""
|
|
return commandResult{}
|
|
}
|
|
return commandResult{output: invalidInput(cmd)}
|
|
}
|
|
|
|
func (s *iosState) cmdHelp() commandResult {
|
|
cmds := commandsForMode(s.mode)
|
|
var b strings.Builder
|
|
for _, e := range cmds {
|
|
if e.name == "?" {
|
|
continue
|
|
}
|
|
b.WriteString(fmt.Sprintf(" %-20s %s\n", e.name, helpText(e.name)))
|
|
}
|
|
return commandResult{output: b.String()}
|
|
}
|
|
|
|
func helpText(name string) string {
|
|
switch name {
|
|
case "show":
|
|
return "Show running system information"
|
|
case "enable":
|
|
return "Turn on privileged commands"
|
|
case "disable":
|
|
return "Turn off privileged commands"
|
|
case "exit":
|
|
return "Exit from the EXEC"
|
|
case "configure":
|
|
return "Enter configuration mode"
|
|
case "write":
|
|
return "Write running configuration to memory"
|
|
case "copy":
|
|
return "Copy from one file to another"
|
|
case "reload":
|
|
return "Halt and perform a cold restart"
|
|
case "terminal":
|
|
return "Set terminal line parameters"
|
|
case "hostname":
|
|
return "Set system's network name"
|
|
case "interface":
|
|
return "Select an interface to configure"
|
|
case "ip":
|
|
return "Global IP configuration subcommands"
|
|
case "no":
|
|
return "Negate a command or set its defaults"
|
|
case "end":
|
|
return "Exit from configure mode"
|
|
case "description":
|
|
return "Interface specific description"
|
|
case "shutdown":
|
|
return "Shutdown the selected interface"
|
|
case "switchport":
|
|
return "Set switching mode characteristics"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func invalidInput(input string) string {
|
|
return fmt.Sprintf("%% Invalid input detected at '^' marker.\n\n%s\n^", input)
|
|
}
|