commit 3c9ba66295db4fbbabaf113f7351463ab6e18cc0 Author: Ambrose Chua Date: Tue Jun 16 15:35:55 2020 +0800 Initial working serial port writer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..095d898 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +apply diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..b6ed697 --- /dev/null +++ b/conf.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "time" +) + +type Device interface { + ReadTimeout(time.Duration) ([]byte, error) + WriteLine([]byte) error + Close() error +} + +var ErrorDeviceOperationNotSupported = errors.New("device operation not supported") + +type ConfigurationLine interface { + Apply(Device) error +} + +type ConfigurationDirective struct { + Name string + Argument string +} + +type ConfigurationSimple []byte + +func (d ConfigurationDirective) Apply(dev Device) error { + if d.Name == "sleep" { + seconds := 0 + _, err := fmt.Sscanf(string(d.Argument), "%d", &seconds) + if err != nil { + return fmt.Errorf("bad sleep arguments: %w", err) + } + time.Sleep(time.Duration(seconds) * time.Second) + } + return nil +} + +func (s ConfigurationSimple) Apply(dev Device) error { + err := dev.WriteLine([]byte(s)) + if err != nil { + return err + } + response, err := dev.ReadTimeout(200 * time.Millisecond) + if errors.Is(err, ErrorDeviceOperationNotSupported) { + } else if err != nil { + return err + } + fmt.Printf("%s", response) + return nil +} + +type ConfigurationReader bufio.Reader + +func NewConfigurationReader(r io.Reader) *ConfigurationReader { + return (*ConfigurationReader)(bufio.NewReader(r)) +} + +func (c *ConfigurationReader) Read() (ConfigurationLine, error) { + line, err := (*bufio.Reader)(c).ReadBytes('\n') + if err != nil { + return nil, err + } + line = bytes.TrimSuffix(line, []byte{'\n'}) + + if len(line) > 1 && line[0] == '!' && line[1] == ' ' { + // Comment + return ConfigurationSimple(string(line)), nil + } else if len(line) > 0 && line[0] == '!' { + // Directive + line = line[1:] + + firstSep := bytes.IndexByte(line, ' ') + if firstSep < 0 { + return ConfigurationDirective{ + Name: string(line), + }, nil + } + + directive := line[:firstSep] + rest := line[firstSep+1:] + return ConfigurationDirective{ + Name: string(directive), + Argument: string(rest), + }, nil + } else { + return ConfigurationSimple(line), nil + } +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..ac52c8e --- /dev/null +++ b/file.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +type DeviceFile struct { + file *os.File +} + +func NewDeviceFile(name string) (DeviceFile, error) { + file, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return DeviceFile{}, err + } + return DeviceFile{ + file: file, + }, nil +} + +func (f DeviceFile) ReadTimeout(timeout time.Duration) ([]byte, error) { + return nil, fmt.Errorf("%w: Read", ErrorDeviceOperationNotSupported) +} + +func (f DeviceFile) WriteLine(buf []byte) error { + buf = append(buf, '\n') + f.file.Write(buf) + return nil +} + +func (f DeviceFile) Close() error { + return f.file.Close() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1adb441 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.makerforce.io/dump/sit/cs2203/confs/apply + +go 1.14 + +require ( + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 + go.bug.st/serial v1.1.0 + golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..79d5939 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/creack/goselect v0.1.1 h1:tiSSgKE1eJtxs1h/VgGQWuXUP0YS4CDIFMp6vaI1ls0= +github.com/creack/goselect v0.1.1/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +go.bug.st/serial v1.1.0 h1:O0EHZw8ZdhmTAikak5ZY/8vyKCpFxZYgqZw1bGegxU8= +go.bug.st/serial v1.1.0/go.mod h1:rpXPISGjuNjPTRTcMlxi9lN6LoIPxd1ixVjBd8aSk/Q= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a25c344 --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +/* +apply interacts with a serial port to configure a Cisco product using files. + +This tool does not attempt to understand the proprietary donkey format that is +the Cisco CLI. Instead, it provides some directives around it to make life +easier when crafting configuration files. + +Basic usage is very simple: Pass a file containing Cisco commands and it will +write it out to serial port. It also prints the output back to stdout for +visual inspection of success. + +apply also contains some additional features to help you in handling files. The +main feature is directives that perform certain actions. These are expressed +within a line beginning with "!". + +Directives + + !sleep + +Sleep a fixed number of seconds. + + !assert + +Ensure that in the next 1 second, output from the serial port contains . + +*/ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "time" +) + +var Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-port PORT] [-writefile] FILE...\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + port := flag.String("port", "/dev/ttyUSB0", "Serial port to write to") + writefile := flag.Bool("writefile", false, "Treat PORT as file, and write to file") + flag.Parse() + confs := flag.Args() + + if len(confs) < 1 { + Usage() + return + } + + var device Device + var err error + if *writefile { + device, err = NewDeviceFile(*port) + } else { + device, err = NewDeviceSerial(*port) + } + if err != nil { + panic(err) + } + defer device.Close() + + for _, conf := range confs { + file, err := os.Open(conf) + if err != nil { + panic(fmt.Errorf("file %s: %w", conf, err)) + } + + reader := NewConfigurationReader(file) + lineNum := 0 + + var line ConfigurationLine + for { + lineNum++ + + line, err = reader.Read() + if err != nil { + break + } + if _, ok := line.(ConfigurationSimple); !ok { + fmt.Printf("directive: %v\n", line) + } + + err = line.Apply(device) + if err != nil { + break + } + } + + if err != nil && err != io.EOF { + panic(fmt.Errorf("file %s line %d: %w", conf, lineNum, err)) + } + + // Attempt to read rest of output + response, err := device.ReadTimeout(1000 * time.Millisecond) + if errors.Is(err, ErrorDeviceOperationNotSupported) { + } else if err != nil { + panic(fmt.Errorf("device: %w", err)) + } + fmt.Printf("%s", response) + + if err := file.Close(); err != nil { + panic(fmt.Errorf("file %s: %w", conf, err)) + } + } +} diff --git a/serial.go b/serial.go new file mode 100644 index 0000000..f00edd5 --- /dev/null +++ b/serial.go @@ -0,0 +1,64 @@ +package main + +import ( + "time" + + "go.bug.st/serial" +) + +type DeviceSerial struct { + port serial.Port + lines chan []byte + lineErr chan error +} + +func NewDeviceSerial(name string) (DeviceSerial, error) { + serialMode := &serial.Mode{ + BaudRate: 9600, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, + } + port, err := serial.Open(name, serialMode) + if err != nil { + return DeviceSerial{}, err + } + lines, lineErr := lineChannel(port) + return DeviceSerial{ + port: port, + lines: lines, + lineErr: lineErr, + }, nil +} + +func (s DeviceSerial) ReadTimeout(timeout time.Duration) ([]byte, error) { + var err error + buf := make([]byte, 0) + t := timerChannel(timeout) + for { + // Read lines until timeout + select { + case line := <-s.lines: + buf = append(buf, line...) + case err = <-s.lineErr: + return buf, err + case <-t: + return buf, err + } + } + return nil, nil +} + +func (s DeviceSerial) WriteLine(buf []byte) error { + // TODO: handle n + buf = append(buf, '\r') + _, err := s.port.Write(buf) + if err != nil { + return err + } + return nil +} + +func (s DeviceSerial) Close() error { + return s.port.Close() +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..9cd9176 --- /dev/null +++ b/util.go @@ -0,0 +1,39 @@ +package main + +import ( + "bufio" + "io" + "time" +) + +func lineChannel(r io.Reader) (chan []byte, chan error) { + lineChan := make(chan []byte, 100) + errChan := make(chan error) + + reader := bufio.NewReader(r) + go func() { + for { + line, err := reader.ReadBytes('\n') + lineChan <- line + + if err != nil { + errChan <- err + break + } + } + close(lineChan) + }() + + return lineChan, errChan +} + +func timerChannel(t time.Duration) chan bool { + timerChan := make(chan bool) + + go func() { + time.Sleep(t) + timerChan <- true + }() + + return timerChan +}