├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── mtbyteproto.go ├── mtbyteproto_test.go ├── protocol.go └── protocol_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | *.goecho -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Netwurx LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | routeros-api-go 2 | =============== 3 | 4 | Go library to manage Mikrotik routers using the Mikrotik RouterOS API 5 | 6 | [![GoDoc](https://godoc.org/github.com/jda/routeros-api-go?status.png)](http://godoc.org/github.com/jda/routeros-api-go) 7 | 8 | # Usage 9 | ```go 10 | import ( 11 | "github.com/jda/routeros-api-go" 12 | "fmt" 13 | ) 14 | c, err := routeros.New("10.0.0.1:8728") 15 | if err != nil { 16 | fmt.Errorf("Error parsing address: %s\n", err) 17 | } 18 | 19 | err = c.Connect("username", "password") 20 | if err != nil { 21 | fmt.Errorf("Error connecting to device: %s\n", err) 22 | } 23 | 24 | res, err := c.Call("/system/resource/getall", nil) 25 | if err != nil { 26 | fmt.Errorf("Error getting system resources: %s\n", err) 27 | } 28 | 29 | uptime := res.SubPairs[0]["uptime"] 30 | fmt.Printf("Uptime: %s\n", uptime) 31 | ``` 32 | 33 | # Running Tests 34 | You need a device or VM running Mikrotik RouterOS to run tests. Mikrotik provides VM images of RouterOS under the [Cloud Hosted Router](http://www.mikrotik.com/download#chr)(CHR) brand. The free edition of CHR is limited to 1Mbps per interface which is more than sufficient for API testing. 35 | 36 | I run the VMDK under VMware Fusion with host-only networking. The CHR images are running DHCP client by default so there's no network setup required, just log in to the image and "/ip address print" to discover which address to use. Change the password for the admin user from blank to admin (or whatever you chose): "/user set 0 password=admin" 37 | 38 | ## Test setup 39 | export ROS_TEST_TARGET=VM_IP:API_PORT 40 | export ROS_TEST_USER=admin 41 | export ROS_TEST_PASSWORD=admin 42 | 43 | ## To Do 44 | * Write better docstrings 45 | * Make README 46 | * Add support for command/response tags 47 | * Add checking for error codes 48 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package routeros 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/tls" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "strings" 12 | ) 13 | 14 | // A reply can contain multiple pairs. A pair is a string key->value. 15 | // A reply can also contain subpairs, that is, a array of pair arrays. 16 | type Reply struct { 17 | Pairs []Pair 18 | SubPairs []map[string]string 19 | } 20 | 21 | func (r *Reply) GetPairVal(key string) (string, error) { 22 | for _, p := range r.Pairs { 23 | if p.Key == key { 24 | return p.Value, nil 25 | } 26 | } 27 | return "", errors.New("key not found") 28 | } 29 | 30 | func (r *Reply) GetSubPairByName(key string) (map[string]string, error) { 31 | for _, p := range r.SubPairs { 32 | if _, ok := p["name"]; ok { 33 | if p["name"] == key { 34 | return p, nil 35 | } 36 | } 37 | } 38 | return nil, errors.New("key not found") 39 | } 40 | 41 | func GetPairVal(pairs []Pair, key string) (string, error) { 42 | for _, p := range pairs { 43 | if p.Key == key { 44 | return p.Value, nil 45 | } 46 | } 47 | return "", errors.New("key not found") 48 | } 49 | 50 | // Client is a RouterOS API client. 51 | type Client struct { 52 | // Network Address. 53 | // E.g. "10.0.0.1:8728" or "router.example.com:8728" 54 | address string 55 | user string 56 | password string 57 | debug bool // debug logging enabled 58 | ready bool // Ready for work (login ok and connection not terminated) 59 | conn net.Conn // Connection to pass around 60 | TLSConfig *tls.Config 61 | } 62 | 63 | // Pair is a Key-Value pair for RouterOS Attribute, Query, and Reply words 64 | // use slices of pairs instead of map because we care about order 65 | type Pair struct { 66 | Key string 67 | Value string 68 | // Op is used for Query words to signify logical operations 69 | // valid operators are -, =, <, > 70 | // see http://wiki.mikrotik.com/wiki/Manual:API#Queries for details. 71 | Op string 72 | } 73 | 74 | type Query struct { 75 | Pairs []Pair 76 | Op string 77 | Proplist []string 78 | } 79 | 80 | func NewPair(key string, value string) *Pair { 81 | p := new(Pair) 82 | p.Key = key 83 | p.Value = value 84 | return p 85 | } 86 | 87 | // Create a new instance of the RouterOS API client 88 | func New(address string) (*Client, error) { 89 | // basic validation of host address 90 | _, _, err := net.SplitHostPort(address) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | var c Client 96 | c.address = address 97 | 98 | return &c, nil 99 | } 100 | 101 | func (c *Client) Close() { 102 | c.conn.Close() 103 | } 104 | 105 | func (c *Client) Connect(user string, password string) error { 106 | 107 | var err error 108 | if c.TLSConfig != nil { 109 | c.conn, err = tls.Dial("tcp", c.address, c.TLSConfig) 110 | } else { 111 | c.conn, err = net.Dial("tcp", c.address) 112 | } 113 | if err != nil { 114 | return err 115 | } 116 | 117 | // try to log in 118 | res, err := c.Call("/login", nil) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | // handle challenge/response 124 | challengeEnc, err := res.GetPairVal("ret") 125 | if err != nil { 126 | return errors.New("Didn't get challenge from ROS") 127 | } 128 | challenge, err := hex.DecodeString(challengeEnc) 129 | if err != nil { 130 | return err 131 | } 132 | h := md5.New() 133 | io.WriteString(h, "\000") 134 | io.WriteString(h, password) 135 | h.Write(challenge) 136 | resp := fmt.Sprintf("00%x", h.Sum(nil)) 137 | var loginParams []Pair 138 | loginParams = append(loginParams, *NewPair("name", user)) 139 | loginParams = append(loginParams, *NewPair("response", resp)) 140 | 141 | // try to log in again with challenge/response 142 | res, err = c.Call("/login", loginParams) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | if len(res.Pairs) > 0 { 148 | return fmt.Errorf("Unexpected result on login: %+v", res) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (c *Client) Query(command string, q Query) (Reply, error) { 155 | err := c.send(command) 156 | if err != nil { 157 | return Reply{}, err 158 | } 159 | 160 | // Set property list if present 161 | if len(q.Proplist) > 0 { 162 | proplist := fmt.Sprintf("=.proplist=%s", strings.Join(q.Proplist, ",")) 163 | err = c.send(proplist) 164 | if err != nil { 165 | return Reply{}, err 166 | } 167 | } 168 | 169 | // send params if we got them 170 | if len(q.Pairs) > 0 { 171 | for _, v := range q.Pairs { 172 | word := fmt.Sprintf("?%s%s=%s", v.Op, v.Key, v.Value) 173 | c.send(word) 174 | } 175 | 176 | if q.Op != "" { 177 | word := fmt.Sprintf("?#%s", q.Op) 178 | c.send(word) 179 | } 180 | } 181 | 182 | // send terminator 183 | err = c.send("") 184 | if err != nil { 185 | return Reply{}, err 186 | } 187 | 188 | res, err := c.receive() 189 | if err != nil { 190 | return Reply{}, err 191 | } 192 | 193 | return res, nil 194 | } 195 | 196 | func (c *Client) Call(command string, params []Pair) (Reply, error) { 197 | err := c.send(command) 198 | if err != nil { 199 | return Reply{}, err 200 | } 201 | 202 | // send params if we got them 203 | if len(params) > 0 { 204 | for _, v := range params { 205 | word := fmt.Sprintf("=%s=%s", v.Key, v.Value) 206 | c.send(word) 207 | } 208 | } 209 | 210 | // send terminator 211 | err = c.send("") 212 | if err != nil { 213 | return Reply{}, err 214 | } 215 | 216 | res, err := c.receive() 217 | if err != nil { 218 | return Reply{}, err 219 | } 220 | 221 | return res, nil 222 | } 223 | -------------------------------------------------------------------------------- /mtbyteproto.go: -------------------------------------------------------------------------------- 1 | package routeros 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type mtbyteprotoError error 8 | 9 | // Get just one byte because MT's size prefix is overoptimized 10 | func (c *Client) getone() int { 11 | charlet := make([]byte, 1) 12 | _, err := c.conn.Read(charlet) 13 | if err != nil { 14 | panic(mtbyteprotoError(err)) 15 | } 16 | numlet := int(charlet[0]) 17 | return numlet 18 | } 19 | 20 | // Decode RouterOS API Word Size Prefix / Figure out how much to read 21 | // TODO: based on MT Docs. Look for way to make this cleaner later 22 | func (client *Client) getlen() int64 { 23 | c := int64(client.getone()) 24 | 25 | if (c & 0x80) == 0x00 { 26 | 27 | } else if (c & 0xC0) == 0x80 { 28 | c &= ^0xC0 29 | c <<= 8 30 | c += int64(client.getone()) 31 | } else if (c & 0xE0) == 0xC0 { 32 | c &= ^0xE0 33 | c <<= 8 34 | c += int64(client.getone()) 35 | c <<= 8 36 | c += int64(client.getone()) 37 | } else if (c & 0xF0) == 0xE0 { 38 | c &= ^0xF0 39 | c <<= 8 40 | c += int64(client.getone()) 41 | c <<= 8 42 | c += int64(client.getone()) 43 | c <<= 8 44 | c += int64(client.getone()) 45 | } else if (c & 0xF8) == 0xF0 { 46 | c = int64(client.getone()) 47 | c <<= 8 48 | c += int64(client.getone()) 49 | c <<= 8 50 | c += int64(client.getone()) 51 | c <<= 8 52 | c += int64(client.getone()) 53 | } 54 | 55 | return c 56 | } 57 | 58 | // Calculate RouterOS API Word Size Prefix 59 | func prefixlen(l int) *bytes.Buffer { 60 | var b bytes.Buffer 61 | switch { 62 | case l < 0x80: 63 | b.WriteByte(byte(l)) 64 | case l < 0x4000: 65 | b.WriteByte(byte(l>>8) | 0x80) 66 | b.WriteByte(byte(l)) 67 | case l < 0x200000: 68 | b.WriteByte(byte(l>>16) | 0xC0) 69 | b.WriteByte(byte(l >> 8)) 70 | b.WriteByte(byte(l)) 71 | case l < 0x10000000: 72 | b.WriteByte(byte(l>>24) | 0xE0) 73 | b.WriteByte(byte(l >> 16)) 74 | b.WriteByte(byte(l >> 8)) 75 | b.WriteByte(byte(l)) 76 | default: 77 | b.WriteByte(0xF0) 78 | b.WriteByte(byte(l >> 24)) 79 | b.WriteByte(byte(l >> 16)) 80 | b.WriteByte(byte(l >> 8)) 81 | b.WriteByte(byte(l)) 82 | } 83 | return &b 84 | } 85 | -------------------------------------------------------------------------------- /mtbyteproto_test.go: -------------------------------------------------------------------------------- 1 | package routeros 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | mtCodingValue1 = 0x00000001 12 | mtCodingValue2 = 0x00000087 13 | mtCodingValue3 = 0x00004321 14 | mtCodingValue4 = 0x002acdef 15 | mtCodingValue5 = 0x10000080 16 | ) 17 | 18 | var ( 19 | mtCodingSize1 = []byte{0x01} 20 | mtCodingSize2 = []byte{0x80, 0x87} 21 | mtCodingSize3 = []byte{0xC0, 0x43, 0x21} 22 | mtCodingSize4 = []byte{0xE0, 0x2a, 0xcd, 0xef} 23 | mtCodingSize5 = []byte{0xF0, 0x10, 0x00, 0x00, 0x80} 24 | ) 25 | 26 | // Create a test net.Conn type 27 | type testConn struct { 28 | readBuf bytes.Buffer 29 | } 30 | 31 | func (testConn) LocalAddr() net.Addr { return nil } 32 | func (testConn) RemoteAddr() net.Addr { return nil } 33 | func (testConn) SetDeadline(t time.Time) error { return nil } 34 | func (testConn) SetReadDeadline(t time.Time) error { return nil } 35 | func (testConn) SetWriteDeadline(t time.Time) error { return nil } 36 | func (c *testConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) } 37 | func (testConn) Write(b []byte) (int, error) { return 0, nil } 38 | func (testConn) Close() error { return nil } 39 | 40 | // Test Client.getlen decodings 41 | func TestGetlen(t *testing.T) { 42 | c := &testConn{readBuf: *bytes.NewBuffer(mtCodingSize1)} 43 | mc := &Client{conn: c} 44 | len := mc.getlen() 45 | if len != mtCodingValue1 || c.readBuf.Len() != 0 { 46 | t.Errorf("single byte read failed, got %#08x", len) 47 | } 48 | c.readBuf = *bytes.NewBuffer(mtCodingSize2) 49 | len = mc.getlen() 50 | if len != mtCodingValue2 || c.readBuf.Len() != 0 { 51 | t.Errorf("double byte read failed, got %#08x", len) 52 | } 53 | c.readBuf = *bytes.NewBuffer(mtCodingSize3) 54 | len = mc.getlen() 55 | if len != mtCodingValue3 || c.readBuf.Len() != 0 { 56 | t.Errorf("triple byte read failed, got %#08x", len) 57 | } 58 | c.readBuf = *bytes.NewBuffer(mtCodingSize4) 59 | len = mc.getlen() 60 | if len != mtCodingValue4 || c.readBuf.Len() != 0 { 61 | t.Errorf("quad byte read failed, got %#08x", len) 62 | } 63 | c.readBuf = *bytes.NewBuffer(mtCodingSize5) 64 | len = mc.getlen() 65 | if len != mtCodingValue5 || c.readBuf.Len() != 0 { 66 | t.Errorf("penta byte read failed, got %#08x", len) 67 | } 68 | } 69 | 70 | // Test prefixlen encodings 71 | func TestPrefixLen(t *testing.T) { 72 | b := prefixlen(mtCodingValue1).Bytes() 73 | if !bytes.Equal(mtCodingSize1, b) { 74 | t.Errorf("single byte write failed, got %v", b) 75 | } 76 | b = prefixlen(mtCodingValue2).Bytes() 77 | if !bytes.Equal(mtCodingSize2, b) { 78 | t.Errorf("double byte write failed, got %v", b) 79 | } 80 | b = prefixlen(mtCodingValue3).Bytes() 81 | if !bytes.Equal(mtCodingSize3, b) { 82 | t.Errorf("triple byte write failed, got %v", b) 83 | } 84 | b = prefixlen(mtCodingValue4).Bytes() 85 | if !bytes.Equal(mtCodingSize4, b) { 86 | t.Errorf("quad byte write failed, got %v", b) 87 | } 88 | b = prefixlen(mtCodingValue5).Bytes() 89 | if !bytes.Equal(mtCodingSize5, b) { 90 | t.Errorf("penta byte write failed, got %v", b) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | // Package routeros provides a programmatic interface to the Mikrotik RouterOS API 2 | package routeros 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | // Encode and send a single line 11 | func (c *Client) send(word string) error { 12 | bword := []byte(word) 13 | prefix := prefixlen(len(bword)) 14 | 15 | _, err := c.conn.Write(prefix.Bytes()) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | _, err = c.conn.Write(bword) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // Get reply 29 | func (c *Client) receive() (reply Reply, err error) { 30 | defer func() { 31 | r := recover() 32 | if r == nil { 33 | return 34 | } 35 | e, ok := r.(mtbyteprotoError) 36 | if ok { 37 | err = e 38 | return 39 | } 40 | panic(r) 41 | }() 42 | 43 | re := false 44 | done := false 45 | trap := false 46 | subReply := make(map[string]string, 1) 47 | for { 48 | length := c.getlen() 49 | if length == 0 && done { 50 | break 51 | } 52 | 53 | inbuf := make([]byte, length) 54 | n, err := io.ReadAtLeast(c.conn, inbuf, int(length)) 55 | // We don't actually care about EOF, but things like ErrUnspectedEOF we would 56 | if err != nil && err != io.EOF { 57 | return reply, err 58 | } 59 | 60 | // be annoying about reading exactly the correct number of bytes 61 | if int64(n) != length { 62 | return reply, fmt.Errorf("incorrect number of bytes read") 63 | } 64 | 65 | word := string(inbuf) 66 | if word == "!done" { 67 | done = true 68 | continue 69 | } 70 | 71 | if word == "!trap" { // error reply 72 | trap = true 73 | continue 74 | } 75 | 76 | if word == "!re" { // new term so start a new pair 77 | if len(subReply) > 0 { 78 | // we've already used this subreply because it has stuff in it 79 | // so we need to close it out and make a new one 80 | reply.SubPairs = append(reply.SubPairs, subReply) 81 | subReply = make(map[string]string, 1) 82 | } else { 83 | re = true 84 | } 85 | continue 86 | } 87 | 88 | if strings.Contains(word, "=") { 89 | parts := strings.SplitN(word, "=", 3) 90 | var key, val string 91 | if len(parts) == 3 { 92 | key = parts[1] 93 | val = parts[2] 94 | } else { 95 | key = parts[1] 96 | } 97 | 98 | if re { 99 | if key != "" { 100 | subReply[key] = val 101 | } 102 | } else { 103 | var p Pair 104 | p.Key = key 105 | p.Value = val 106 | reply.Pairs = append(reply.Pairs, p) 107 | } 108 | } 109 | } 110 | 111 | if len(subReply) > 0 { 112 | reply.SubPairs = append(reply.SubPairs, subReply) 113 | } 114 | 115 | // if we got a error flag from routeros, look for a message and signal err 116 | if trap { 117 | trapMesasge := "" 118 | for _, v := range reply.Pairs { 119 | if v.Key == "message" { 120 | trapMesasge = v.Value 121 | continue 122 | } 123 | } 124 | 125 | if trapMesasge == "" { 126 | return reply, fmt.Errorf("routeros: unknown error") 127 | } else { 128 | return reply, fmt.Errorf("routeros: %s", trapMesasge) 129 | } 130 | } 131 | 132 | return reply, nil 133 | } 134 | -------------------------------------------------------------------------------- /protocol_test.go: -------------------------------------------------------------------------------- 1 | package routeros 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | type TestVars struct { 10 | Username string 11 | Password string 12 | Address string 13 | } 14 | 15 | // Make sure we have the env vars to run, handle bailing if we don't 16 | func PrepVars(t *testing.T) TestVars { 17 | var tv TestVars 18 | 19 | addr := os.Getenv("ROS_TEST_TARGET") 20 | if addr == "" { 21 | t.Skip("Can't run test because ROS_TEST_TARGET undefined") 22 | } else { 23 | tv.Address = addr 24 | } 25 | 26 | username := os.Getenv("ROS_TEST_USER") 27 | if username == "" { 28 | tv.Username = "admin" 29 | t.Logf("ROS_TEST_USER not defined. Assuming %s\n", tv.Username) 30 | } else { 31 | tv.Username = username 32 | } 33 | 34 | password := os.Getenv("ROS_TEST_PASSWORD") 35 | if password == "" { 36 | tv.Password = "admin" 37 | t.Logf("ROS_TEST_PASSWORD not defined. Assuming %s\n", tv.Password) 38 | } else { 39 | tv.Password = password 40 | } 41 | 42 | return tv 43 | } 44 | 45 | // Test logging in and out 46 | func TestLogin(t *testing.T) { 47 | tv := PrepVars(t) 48 | c, err := New(tv.Address) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | err = c.Connect(tv.Username, tv.Password) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | } 58 | 59 | // Test running a command (uptime) 60 | func TestCommand(t *testing.T) { 61 | tv := PrepVars(t) 62 | c, err := New(tv.Address) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | err = c.Connect(tv.Username, tv.Password) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | res, err := c.Call("/system/resource/getall", nil) 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | uptime := res.SubPairs[0]["uptime"] 78 | t.Logf("Uptime: %s\n", uptime) 79 | } 80 | 81 | // Test querying data (getting IP addresses on ether1) 82 | func TestQuery(t *testing.T) { 83 | tv := PrepVars(t) 84 | c, err := New(tv.Address) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | err = c.Connect(tv.Username, tv.Password) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | getEther1Addrs := NewPair("interface", "ether1") 95 | getEther1Addrs.Op = "=" 96 | var q Query 97 | q.Pairs = append(q.Pairs, *getEther1Addrs) 98 | q.Proplist = []string{"address"} 99 | 100 | res, err := c.Query("/ip/address/print", q) 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | 105 | t.Log("IP addresses on ether1:") 106 | for _, v := range res.SubPairs { 107 | for _, sv := range v { 108 | t.Log(sv) 109 | } 110 | } 111 | } 112 | 113 | // Test adding some bridges (test of Call) 114 | func TestCallAddBridges(t *testing.T) { 115 | tv := PrepVars(t) 116 | c, err := New(tv.Address) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | err = c.Connect(tv.Username, tv.Password) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | for i := 1; i <= 10; i++ { 127 | var pairs []Pair 128 | bName := "test-bridge" + strconv.Itoa(i) 129 | pairs = append(pairs, Pair{Key: "name", Value: bName}) 130 | pairs = append(pairs, Pair{Key: "comment", Value: "test bridge number " + strconv.Itoa(i)}) 131 | pairs = append(pairs, Pair{Key: "arp", Value: "disabled"}) 132 | res, err := c.Call("/interface/bridge/add", pairs) 133 | if err != nil { 134 | t.Errorf("Error adding bridge: %s\n", err) 135 | } 136 | t.Logf("reply from adding bridge: %+v\n", res) 137 | } 138 | } 139 | 140 | // Test getting list of interfaces (test Query) 141 | func TestQueryMultiple(t *testing.T) { 142 | tv := PrepVars(t) 143 | c, err := New(tv.Address) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | err = c.Connect(tv.Username, tv.Password) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | var q Query 154 | q.Pairs = append(q.Pairs, Pair{Key: "type", Value: "bridge", Op: "="}) 155 | 156 | res, err := c.Query("/interface/print", q) 157 | if err != nil { 158 | t.Error(err) 159 | } 160 | if len(res.SubPairs) <= 1 { 161 | t.Error("Did not get multiple SubPairs from bridge interface query") 162 | } 163 | } 164 | 165 | // Test query with proplist 166 | func TestQueryWithProplist(t *testing.T) { 167 | tv := PrepVars(t) 168 | c, err := New(tv.Address) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | err = c.Connect(tv.Username, tv.Password) 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | var q Query 179 | q.Proplist = append(q.Proplist, "name") 180 | q.Proplist = append(q.Proplist, "comment") 181 | q.Proplist = append(q.Proplist, ".id") 182 | q.Pairs = append(q.Pairs, Pair{Key: "type", Value: "bridge", Op: "="}) 183 | res, err := c.Query("/interface/print", q) 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | 188 | for _, b := range res.SubPairs { 189 | t.Logf("Found bridge %s (%s)\n", b["name"], b["comment"]) 190 | 191 | } 192 | } 193 | 194 | // Test query with proplist 195 | func TestCallRemoveBridges(t *testing.T) { 196 | tv := PrepVars(t) 197 | c, err := New(tv.Address) 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | 202 | err = c.Connect(tv.Username, tv.Password) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | var q Query 208 | q.Proplist = append(q.Proplist, ".id") 209 | q.Pairs = append(q.Pairs, Pair{Key: "type", Value: "bridge", Op: "="}) 210 | res, err := c.Query("/interface/print", q) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | 215 | for _, v := range res.SubPairs { 216 | var pairs []Pair 217 | pairs = append(pairs, Pair{Key: ".id", Value: v[".id"]}) 218 | _, err = c.Call("/interface/bridge/remove", pairs) 219 | if err != nil { 220 | t.Errorf("error removing bridge: %s\n", err) 221 | } 222 | } 223 | } 224 | 225 | // Test call that should trigger error response from router 226 | func TestCallCausesError(t *testing.T) { 227 | tv := PrepVars(t) 228 | c, err := New(tv.Address) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | err = c.Connect(tv.Username, tv.Password) 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | 238 | var pairs []Pair 239 | pairs = append(pairs, Pair{Key: "address", Value: "192.168.99.1/32"}) 240 | pairs = append(pairs, Pair{Key: "comment", Value: "this address should never be added"}) 241 | pairs = append(pairs, Pair{Key: "interface", Value: "badbridge99"}) 242 | _, err = c.Call("/ip/address/add", pairs) 243 | if err != nil { 244 | t.Logf("Error adding address to nonexistent bridge: %s\n", err) 245 | } else { 246 | t.Error("did not get error when adding address to nonexistent bridge") 247 | } 248 | } 249 | 250 | // Test query that should trigger error response from router 251 | func TestQueryCausesError(t *testing.T) { 252 | tv := PrepVars(t) 253 | c, err := New(tv.Address) 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | 258 | err = c.Connect(tv.Username, tv.Password) 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | 263 | var q Query 264 | q.Proplist = append(q.Proplist, ".id") 265 | _, err = c.Query("/ip/address/sneeze", q) 266 | if err != nil { 267 | t.Logf("Error querying with nonexistent command: %s\n", err) 268 | } else { 269 | t.Error("did not get error when querying nonexistent command") 270 | } 271 | } 272 | --------------------------------------------------------------------------------