├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── error.go ├── error_test.go ├── host.go ├── index.go ├── mib.go ├── object.go └── table.go ├── build └── docker-push.sh ├── client ├── client.go ├── client_test.go ├── config.go ├── engine.go ├── logging.go ├── options.go ├── request.go ├── transport.go ├── transport_test.go ├── udp.go ├── udp_test.go ├── walk.go └── walk_test.go ├── cmd ├── flags.go ├── logging.go ├── mibs.go ├── snmpbot │ └── main.go ├── snmpget │ └── main.go ├── snmpobject │ └── main.go ├── snmpprobe │ └── main.go ├── snmptable │ └── main.go └── snmpwalk │ └── main.go ├── go.mod ├── go.sum ├── mibs ├── bridge_mib │ ├── syntax_bridge_id.go │ ├── syntax_port_list.go │ └── syntax_stp_port_id.go ├── client.go ├── config.go ├── config_test.go ├── id.go ├── id_test.go ├── index.go ├── logging.go ├── mib.go ├── mib_test.go ├── mibs.go ├── object.go ├── oid_test.go ├── options.go ├── registry.go ├── syntax.go ├── syntax_bits.go ├── syntax_counter.go ├── syntax_displaystring.go ├── syntax_enum.go ├── syntax_gauge.go ├── syntax_integer.go ├── syntax_ip_address.go ├── syntax_mac_address.go ├── syntax_octet_string.go ├── syntax_oid.go ├── syntax_physaddr.go ├── syntax_registry.go ├── syntax_test.go ├── syntax_timeticks.go ├── syntax_unsigned.go ├── table.go └── test │ └── TEST2-MIB.json ├── scripts ├── .gitignore ├── README.md ├── mib-import.py └── requirements.txt ├── server ├── config.go ├── config_test.go ├── engine.go ├── engine_client_test.go ├── engine_hosts.go ├── engine_hosts_test.go ├── engine_test.go ├── host.go ├── host_test.go ├── hosts.go ├── hosts_test.go ├── logging.go ├── mibs.go ├── mibs_test.go ├── object.go ├── object_test.go ├── options.go ├── query.go ├── table.go ├── table_test.go ├── test │ └── TEST-MIB.json ├── web.go └── web_test.go └── snmp ├── bulk_pdu.go ├── generic_pdu.go ├── marshal.go ├── oid.go ├── oid_test.go ├── packet.go ├── packet_test.go ├── pdu.go ├── snmp.go ├── string_test.go ├── unmarshal.go └── varbind.go /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | /.git/ 3 | /vendor/ 4 | /scripts/opt/ 5 | /scripts/mib-import.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swo 2 | .*.swp 3 | 4 | /vendor 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | language: go 6 | go: 7 | - '1.15' 8 | env: 9 | global: 10 | # DOCKER_USERNAME 11 | - secure: Eb4Qe+9FMFqI7KvroqBl3zCEuyjb5f04Khx2iEWAfVrmwCKyt3xKSLQZgLL+q3X3j71girwgtKR1WecC5832E6DpYRrq6itoALNEPJDadE3AcUYMsh1NsrhK5aMxvEev5vwDqhBX+wEayO+pj4r1FY4AYCuR3vcZVj01lxEZEzglRMrPisGbrgWvlKSwp08j/NOTDdmNQ4tcNCYHMUpOjHi9lzCA9FAcH4Jo6TfAKgcX3VZ0hxGLb79xhGBkC5uQplJviQbUKVQinYVAIp41dsI9EDmYX097OigJs+gWZZpyktbGfnu7lDczqd8uLdPPb74dVqhdkqsbVSqo+/5OyexDYAXzf1rB8ciXfbYOZ1ogSBorMoljSaXKatzcrzdItQKCqG/0MNuKM7YL9kNyxjIMx4Jx/Hy1hdmoUWbaHdQPLUiT8ohq9CcmKaACbvaYFRw5/LxzOTXhLVjBK6QlmBr4AYVomPUg+DD+WslZ2tAi7Gt8jUNi+4BJOn8+E17dytVyV5x1fks0ZPsztuv9/4MVQhjFHfU1z9mW+9jxhS+A/7cZFyICtWkcFZGSs8yFuFEIydEX/hfQOOortUnVD24LaDdPev/rtRvBW/rqzQcTKHHRhBFL89E7jKevHQta0nSoeWCoer2+sbgjL5E3VgrPxAX+3A+5kMOkT5JrlyM= 12 | # DOCKER_PASSWORD 13 | - secure: HKvn5AWLEIyEEV+AubCpCZYW89vWyZ32UNxEYN31AMrs12iQ8S9oOHnlR7282R3BCqqTsqAGs7J+KWnKZlT7fWobzf58t/tPyGezZO2WnsvN5IWDZL++IUuKjWMbSfy3rHGJKtJgXDqFgFrdlnhN6ZgZBC7huhxZfLgq8/22ZN1pQ+il1syFAwmPpiYy2Ef8SRRjbRMqw4oxf+fPUAm9RvAhcPL5pbIbNs/N0DEeBhe6Is2PicUE5wKrxT6/Ido6shwv+LbgaS/SG/JB2eLMEnMRn+7o6tcMLisg8oYQD0IWgUrB5UFe0/6z62CDcfr6Wm7Kf9Tw8efWtWVUIS2nf2R0B4svtnQA6hknAGwDulYCWqa92w95V/311O5Fa9eCc5p5/eBBg5tAtLcSxIEgz+qeOtsAcDLT8Z2hxVGDJfKKiUjuIivBKihgPT5EVdb5GFBa967AlmchydHWdUoiI5Rzmr0beK+Dm5MTCsF6xTrnYVEvFSD24UdTZelKbj4E1+doOIda8SFdMigEB82Od4DvHpBWIH0JQykAkvDqzoHJkEIDU2ekBDNhRURvVzXgrPrXJjBfJxwXuuXvhDhz45xp6FBvhqqKl0jfiDn7FR+8vCE2c0RbPe38IXAi8kdjAaKsOpRpRW2X8VG9CZr0z0TaN9s/qNBsyxoll0jUmRI= 14 | 15 | install: go mod download 16 | script: 17 | - "! gofmt -l . | grep ." 18 | - go vet -composites=false ./... 19 | - go test -v ./... 20 | 21 | before_deploy: 22 | - docker build -t qmsk/snmpbot . 23 | deploy: 24 | - provider: script 25 | skip_cleanup: true 26 | script: ./build/docker-push.sh 27 | on: 28 | tags: true 29 | go: '1.15' 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## build go backend 2 | FROM golang:1.15-buster as go-build 3 | 4 | WORKDIR /go/src/github.com/qmsk/snmpbot 5 | 6 | # dependencies 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | # source code 11 | COPY . ./ 12 | RUN go install -v ./cmd/... 13 | 14 | 15 | ## download mibs 16 | FROM buildpack-deps:stretch-scm as get-mibs 17 | 18 | ARG SNMPBOT_MIBS_VERSION=0.1.0 19 | 20 | RUN curl -fsSL https://github.com/qmsk/snmpbot-mibs/archive/v${SNMPBOT_MIBS_VERSION}.tar.gz | tar -C /tmp -xzv 21 | 22 | 23 | ## runtime 24 | # must match with go-build base image 25 | FROM debian:stretch 26 | 27 | RUN adduser --system --home /opt/qmsk/snmpbot --uid 1000 --gid 100 qmsk-snmpbot 28 | 29 | RUN mkdir -p \ 30 | /opt/qmsk/snmpbot \ 31 | /opt/qmsk/snmpbot/bin \ 32 | /opt/qmsk/snmpbot/mibs 33 | 34 | COPY --from=go-build /go/bin/snmp* /opt/qmsk/snmpbot/bin/ 35 | COPY --from=get-mibs /tmp/snmpbot-mibs-* /opt/qmsk/snmpbot/mibs/ 36 | 37 | USER qmsk-snmpbot 38 | ENV \ 39 | PATH=$PATH:/opt/qmsk/snmpbot/bin \ 40 | SNMPBOT_MIBS=/opt/qmsk/snmpbot/mibs 41 | 42 | CMD ["/opt/qmsk/snmpbot/bin/snmpbot", \ 43 | "--http-listen=:8286", \ 44 | "--verbose" \ 45 | ] 46 | EXPOSE 8286/tcp 47 | -------------------------------------------------------------------------------- /api/error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Error struct { 9 | Error error 10 | } 11 | 12 | // Custom JSON Marshal function for error type 13 | // Formats errors to strings 14 | func (err Error) MarshalJSON() ([]byte, error) { 15 | return json.Marshal(err.Error.Error()) 16 | } 17 | 18 | // Custom JSON Unmarshal function for error type 19 | // error string back to Error 20 | func (err *Error) UnmarshalJSON(data []byte) error { 21 | var errorMessage string 22 | 23 | if errors := json.Unmarshal(data, &errorMessage); errors != nil { 24 | return errors 25 | } 26 | 27 | err.Error = fmt.Errorf(errorMessage) 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /api/error_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type TestStruct struct { 10 | Message Error `json:"error"` 11 | } 12 | 13 | func TestErrorJSONMarshal(t *testing.T) { 14 | ts := TestStruct{Message: Error{fmt.Errorf("Test error")}} 15 | var testData []byte 16 | var err error 17 | testData, err = json.Marshal(ts) 18 | 19 | if err != nil { 20 | t.Errorf("Failed to marshal Error") 21 | } 22 | 23 | if string(testData) != "{\"error\":\"Test error\"}" { 24 | t.Errorf("Unexpected JSON marshalled string %s", string(testData)) 25 | } 26 | } 27 | 28 | func TestErrorJsonUnmarshal(t *testing.T) { 29 | var testData []byte = []byte("{\"error\": \"Test error\"}") 30 | var target TestStruct 31 | err := json.Unmarshal(testData, &target) 32 | if err != nil { 33 | t.Errorf("Failed to unmarshal error") 34 | } 35 | if target.Message.Error.Error() != "Test error" { 36 | t.Errorf("Error message not same as in JSON") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api/host.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type IndexHosts struct { 4 | Hosts []HostIndex 5 | } 6 | 7 | // Shallow host metadata (only configuration) 8 | // 9 | // * `GET /api/ => { "Hosts": [ { ... } ] }` 10 | // * `GET /api/hosts/ => [ { ... } ]` 11 | // * `GET /api/hosts/:id => { ... }` 12 | type HostIndex struct { 13 | ID string 14 | SNMP string 15 | Online bool 16 | Location string `json:",omitempty"` 17 | Error *Error `json:",omitempty"` 18 | } 19 | 20 | // Optional URL ?query params 21 | // 22 | // * `GET /api/hosts/:host` 23 | type HostQuery struct { 24 | SNMP string `schema:"snmp"` 25 | Community string `schema:"community"` 26 | } 27 | 28 | // Dynamic host configuration 29 | // 30 | // The host may or may not be configured yet. 31 | // 32 | // * `PUT /api/hosts/:id` 33 | type HostPUT struct { 34 | SNMP string `schema:"snmp"` 35 | Community string `schema:"community"` 36 | Location string `schema:"location"` 37 | } 38 | 39 | // Dynamic host configuration 40 | // 41 | // The ID must be unique (must not already be configured). 42 | // 43 | // * `POST /api/hosts/` 44 | type HostPOST struct { 45 | ID string `schema:"id"` 46 | SNMP string `schema:"snmp"` 47 | Community string `schema:"community"` 48 | Location string `schema:"location"` 49 | } 50 | 51 | // Deep host metadata (individual mibs/objects/tables) 52 | // 53 | // * `GET /api/hosts/:host/ => { ... }` 54 | type Host struct { 55 | HostIndex 56 | 57 | MIBs []MIBIndex 58 | Objects []ObjectIndex 59 | Tables []TableIndex 60 | } 61 | -------------------------------------------------------------------------------- /api/index.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Metadata 4 | // 5 | // * `GET /api/ => { ... }` 6 | type Index struct { 7 | Hosts []HostIndex 8 | MIBs []MIBIndex 9 | 10 | IndexObjects 11 | IndexTables 12 | } 13 | -------------------------------------------------------------------------------- /api/mib.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // MIB identifier 4 | // 5 | // * `GET /api/ => { "MIBs": [ { ... } ] }` 6 | // * `GET /api/mibs/ => [ { ... } ]` 7 | type MIBIndex struct { 8 | ID string 9 | } 10 | 11 | // MIB metadata 12 | // 13 | // * `GET /api/mibs/:mib => { ... }` 14 | type MIB struct { 15 | MIBIndex 16 | 17 | Objects []ObjectIndex 18 | Tables []TableIndex 19 | } 20 | -------------------------------------------------------------------------------- /api/object.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type IndexObjects struct { 4 | Objects []ObjectIndex 5 | } 6 | 7 | // Object metadata 8 | // 9 | // Different Hosts can have different `Objects` depending on what MIBs were probed. 10 | // 11 | // * `GET /api/ => { "Objects": [ ... ] }` 12 | // * `GET /api/mibs/ => [ { "Objects": [ ... ] } ]` 13 | // * `GET /api/mibs/:mib => { "Objects": [ ... ] }` 14 | // * `GET /api/hosts/:host/ => { "Objects": [ ... ] }` 15 | // * `GET /api/objects => { "Objects": [ ... ] }` 16 | type ObjectIndex struct { 17 | ID string 18 | IndexKeys []string `json:",omitempty"` 19 | } 20 | 21 | type ObjectInstance struct { 22 | HostID string 23 | Index ObjectIndexMap `json:",omitempty"` 24 | Value interface{} `json:",omitempty"` 25 | } 26 | 27 | type ObjectError struct { 28 | HostID string 29 | Index ObjectIndexMap `json:",omitempty"` 30 | Value interface{} `json:",omitempty"` 31 | Error Error 32 | } 33 | 34 | // Object data 35 | // 36 | // Normal non-tabular objects will only have a single `Instances` entry without any `Index` field. 37 | // 38 | // The same `Object` can contain `Instances` for multiple different `HostID`s! 39 | // 40 | // * `GET /api/objects/ => [ { ... }, ... ]` 41 | // * `GET /api/objects/:object => { ... }` 42 | // 43 | // * `GET /api/hosts/:host/objects/ => [ { ... }, ... ]` 44 | // * `GET /api/hosts/:host/objects/:object => { ... }` 45 | type Object struct { 46 | ObjectIndex 47 | Instances []ObjectInstance 48 | Errors []ObjectError `json:",omitempty"` 49 | } 50 | 51 | type ObjectIndexMap map[string]interface{} 52 | 53 | // Optional URL ?query params 54 | // 55 | // Multiple values for the same field are OR, multiple fields are AND. 56 | // 57 | // * `GET /api/objects/:object` 58 | // * `GET /api/hosts/:host/objects/:object` 59 | type ObjectQuery struct { 60 | Hosts []string `schema:"host"` 61 | } 62 | 63 | // Optional URL ?query params 64 | // 65 | // Multiple values for the same field are OR, multiple fields are AND. 66 | // 67 | // * `GET /api/objects/` 68 | // * `GET /api/hosts/:host/objects/` 69 | type ObjectsQuery struct { 70 | Hosts []string `schema:"host"` 71 | Objects []string `schema:"object"` 72 | Tables []string `schema:"table"` 73 | } 74 | -------------------------------------------------------------------------------- /api/table.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type IndexTables struct { 4 | Tables []TableIndex 5 | } 6 | 7 | // Table metadata 8 | // 9 | // Different Hosts can have different `Tables` depending on what MIBs were probed. 10 | // 11 | // * `GET /api/ => { "Tables": [ ... ] }` 12 | // * `GET /api/mibs/ => [ { "Tables": [ ... ] } ]` 13 | // * `GET /api/mibs/:mib => { "Tables": [ ... ] }` 14 | // * `GET /api/hosts/:host/ => { "Tables": [ ... ] }` 15 | // * `GET /api/tables => { "Tables": [ ... ] }` 16 | type TableIndex struct { 17 | ID string 18 | 19 | IndexKeys []string 20 | ObjectKeys []string 21 | } 22 | 23 | // Table data 24 | // 25 | // The same `Table` can contain `Entries` for multiple different `HostID`s! 26 | // 27 | // * `GET /api/tables/ => [ { ... }, ... ]` 28 | // * `GET /api/tables/:table => { ... }` 29 | // * `GET /api/hosts/:host/tables/ => [ { ... }, ... ]` 30 | // * `GET /api/hosts/:host/tables/:table => { ... }` 31 | type Table struct { 32 | TableIndex 33 | 34 | Entries []TableEntry 35 | Errors []TableError `json:",omitempty"` 36 | } 37 | 38 | type TableIndexMap map[string]interface{} 39 | type TableObjectsMap map[string]interface{} 40 | 41 | type TableEntry struct { 42 | HostID string `json:",omitempty"` // XXX: always? 43 | Index TableIndexMap 44 | Objects TableObjectsMap 45 | Errors []Error `json:",omitempty"` 46 | } 47 | 48 | type TableError struct { 49 | HostID string `json:",omitempty"` 50 | Error Error 51 | } 52 | 53 | // Optional URL ?query params 54 | // 55 | // Multiple values for the same field are OR, multiple fields are AND. 56 | // 57 | // * `GET /api/tables/:table` 58 | // * `GET /api/hosts/:host/tables/:table` 59 | type TableQuery struct { 60 | Hosts []string `schema:"host"` 61 | Objects []string `schema:"object"` 62 | } 63 | 64 | // Optional URL ?query params 65 | // 66 | // Multiple values for the same field are OR, multiple fields are AND. 67 | // 68 | // * `GET /api/tables/` 69 | // * `GET /api/hosts/:host/tables/` 70 | type TablesQuery struct { 71 | Hosts []string `schema:"host"` 72 | Tables []string `schema:"table"` 73 | Objects []string `schema:"object"` 74 | } 75 | -------------------------------------------------------------------------------- /build/docker-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | set -ue 4 | 5 | VERSION=${TRAVIS_TAG#v} 6 | 7 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 8 | 9 | docker tag qmsk/snmpbot qmsk/snmpbot:$VERSION 10 | 11 | docker push qmsk/snmpbot:latest 12 | docker push qmsk/snmpbot:$VERSION 13 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/go-logging" 6 | "github.com/qmsk/snmpbot/snmp" 7 | "net" 8 | ) 9 | 10 | func NewClient(engine *Engine, config Config) (*Client, error) { 11 | var client = makeClient(engine, config.Options) 12 | 13 | if addr, err := engine.transport.Resolve(config.Address); err != nil { 14 | return nil, fmt.Errorf("Resolve Config.Address=%v: %v", config.Address, err) 15 | } else { 16 | client.addr = addr 17 | } 18 | 19 | client.log = logging.WithPrefix(log, fmt.Sprintf("Client<%v>", &client)) 20 | 21 | return &client, nil 22 | } 23 | 24 | func makeClient(engine *Engine, options Options) Client { 25 | return Client{ 26 | engine: engine, 27 | options: options, 28 | } 29 | } 30 | 31 | type Client struct { 32 | engine *Engine 33 | options Options 34 | log logging.PrefixLogging 35 | 36 | addr net.Addr // host or host:port 37 | } 38 | 39 | func (client *Client) String() string { 40 | return fmt.Sprintf("%v@%v", string(client.options.Community), client.addr) 41 | } 42 | 43 | func (client *Client) request(send IO) (IO, error) { 44 | var request = NewRequest(client.options, send) 45 | 46 | if err := client.engine.Request(request); err != nil { 47 | client.log.Infof("Request %v: %v", request, err) 48 | 49 | return IO{}, err 50 | 51 | } else if recv, err := request.Result(); err != nil { 52 | client.log.Infof("Request %v: %v", request, err) 53 | 54 | return recv, err 55 | } else { 56 | client.log.Infof("Request %v", request) 57 | 58 | return recv, nil 59 | } 60 | } 61 | 62 | func (client *Client) requestPDU(requestType snmp.PDUType, pdu snmp.PDU, responseType snmp.PDUType) ([]snmp.VarBind, error) { 63 | var send = IO{ 64 | Addr: client.addr, 65 | Packet: snmp.Packet{ 66 | Version: SNMPVersion, 67 | Community: []byte(client.options.Community), 68 | }, 69 | PDUMeta: snmp.PDUMeta{ 70 | PDUType: requestType, 71 | }, 72 | PDU: pdu, 73 | } 74 | 75 | if recv, err := client.request(send); err != nil { 76 | return nil, err 77 | } else if recv.PDUType != responseType { 78 | return nil, fmt.Errorf("Invalid %v response type, expected %v, got %v", requestType, responseType, recv.PDUType) 79 | } else if responsePDU, ok := recv.PDU.(snmp.GenericPDU); !ok { 80 | return nil, fmt.Errorf("Invalid %v response type, expected %v, got %v with PDU of type %T", requestType, responseType, recv.PDUType, recv.PDU) 81 | } else { 82 | return responsePDU.VarBinds, nil 83 | } 84 | } 85 | 86 | func (client *Client) requestGeneric(requestType snmp.PDUType, varBinds []snmp.VarBind, responseType snmp.PDUType) ([]snmp.VarBind, error) { 87 | var pdu = snmp.GenericPDU{ 88 | VarBinds: varBinds, 89 | } 90 | 91 | if len(varBinds) == 0 { 92 | return nil, nil 93 | } else if varBinds, err := client.requestPDU(requestType, pdu, responseType); err != nil { 94 | return nil, err 95 | } else if len(varBinds) != len(varBinds) { 96 | return varBinds, fmt.Errorf("Invalid %v response, expected %d vars, got %v with %d vars", requestType, len(varBinds), responseType, len(varBinds)) 97 | } else { 98 | return varBinds, nil 99 | } 100 | } 101 | 102 | // Split request OIDs into multiple requests of options.MaxVars each. 103 | // 104 | // Override response varbinds outside of rootOIDs with snmp.EndOfMibViewValue 105 | // 106 | // TODO: automatically handle snmp.TooBigError? 107 | func (client *Client) requestSplit(requestType snmp.PDUType, varBinds []snmp.VarBind, responseType snmp.PDUType) ([]snmp.VarBind, error) { 108 | var maxVars = DefaultMaxVars 109 | var retVars = make([]snmp.VarBind, len(varBinds)) 110 | var retLen = uint(0) 111 | 112 | if client.options.MaxVars > 0 { 113 | maxVars = client.options.MaxVars 114 | } 115 | 116 | for retLen < uint(len(varBinds)) { 117 | var reqOffset = retLen 118 | var reqVars = make([]snmp.VarBind, maxVars) 119 | var reqLen = uint(0) 120 | 121 | for retLen+reqLen < uint(len(varBinds)) && reqLen < maxVars { 122 | reqVars[reqLen] = varBinds[reqOffset+reqLen] 123 | reqLen++ 124 | } 125 | 126 | if varBinds, err := client.requestGeneric(requestType, reqVars[:reqLen], responseType); err != nil { 127 | return nil, err 128 | } else { 129 | for _, varBind := range varBinds { 130 | retVars[retLen] = varBind 131 | retLen++ 132 | } 133 | } 134 | } 135 | 136 | return retVars, nil 137 | } 138 | 139 | func makeGetVars(oids []snmp.OID) []snmp.VarBind { 140 | var varBinds = make([]snmp.VarBind, len(oids)) 141 | 142 | for i, oid := range oids { 143 | varBinds[i] = snmp.MakeVarBind(oid, nil) 144 | } 145 | 146 | return varBinds 147 | } 148 | 149 | func (client *Client) Get(oids ...snmp.OID) ([]snmp.VarBind, error) { 150 | return client.requestGeneric(snmp.GetRequestType, makeGetVars(oids), snmp.GetResponseType) 151 | } 152 | 153 | func (client *Client) GetNext(oids ...snmp.OID) ([]snmp.VarBind, error) { 154 | return client.requestGeneric(snmp.GetNextRequestType, makeGetVars(oids), snmp.GetResponseType) 155 | } 156 | 157 | func (client *Client) GetNextSplit(oids []snmp.OID) ([]snmp.VarBind, error) { 158 | return client.requestSplit(snmp.GetNextRequestType, makeGetVars(oids), snmp.GetResponseType) 159 | } 160 | 161 | func (client *Client) getBulkMaxRepetitions(scalarsLen uint, entriesLen uint) uint { 162 | var maxRepetitions = DefaultMaxRepetitions 163 | var maxVars = DefaultMaxVars 164 | 165 | if client.options.MaxRepetitions != 0 { 166 | maxRepetitions = client.options.MaxRepetitions 167 | } 168 | if client.options.MaxVars != 0 { 169 | maxVars = client.options.MaxVars 170 | } 171 | 172 | if scalarsLen >= maxVars || entriesLen >= maxVars-scalarsLen { 173 | return 1 174 | } else if scalarsLen+maxRepetitions*entriesLen > maxVars { 175 | return (maxVars - scalarsLen) / entriesLen 176 | } else { 177 | return maxRepetitions 178 | } 179 | } 180 | 181 | func makeBulkVars(scalars []snmp.OID, entries []snmp.OID) []snmp.VarBind { 182 | var varBinds = make([]snmp.VarBind, len(scalars)+len(entries)) 183 | 184 | for i, oid := range scalars { 185 | varBinds[i] = snmp.MakeVarBind(oid, nil) 186 | } 187 | for i, oid := range entries { 188 | varBinds[len(scalars)+i] = snmp.MakeVarBind(oid, nil) 189 | } 190 | 191 | return varBinds 192 | } 193 | 194 | func unpackBulkVars(scalarCount int, entryLen int, varBinds []snmp.VarBind) ([]snmp.VarBind, [][]snmp.VarBind, error) { 195 | var scalarVars = varBinds[:scalarCount] 196 | var entryCount = (len(varBinds) - scalarCount) / entryLen 197 | var entryList = make([][]snmp.VarBind, entryCount) 198 | 199 | if len(varBinds) < scalarCount+entryLen { 200 | return nil, nil, fmt.Errorf("Invalid bulk response for %d+%d => %d vars", scalarCount, entryLen, len(varBinds)) 201 | } 202 | 203 | for i := 0; i < entryCount; i++ { 204 | var enrtryVars = make([]snmp.VarBind, entryLen) 205 | 206 | for j := 0; j < entryLen; j++ { 207 | enrtryVars[j] = varBinds[scalarCount+i*entryLen+j] 208 | } 209 | 210 | entryList[i] = enrtryVars 211 | } 212 | 213 | return scalarVars, entryList, nil 214 | 215 | } 216 | 217 | func (client *Client) GetBulk(scalars []snmp.OID, entries []snmp.OID) ([]snmp.VarBind, [][]snmp.VarBind, error) { 218 | var pdu = snmp.BulkPDU{ 219 | NonRepeaters: len(scalars), 220 | MaxRepetitions: int(client.getBulkMaxRepetitions(uint(len(scalars)), uint(len(entries)))), 221 | VarBinds: makeBulkVars(scalars, entries), 222 | } 223 | 224 | if len(pdu.VarBinds) == 0 { 225 | return nil, nil, nil 226 | } 227 | 228 | if varBinds, err := client.requestPDU(snmp.GetBulkRequestType, pdu, snmp.GetResponseType); err != nil { 229 | return nil, nil, err 230 | } else { 231 | return unpackBulkVars(len(scalars), len(entries), varBinds) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type Config struct { 8 | Options // overrides community from URL user@ 9 | Address string // host or host:port from URL 10 | Object string // optional object from URL /path 11 | } 12 | 13 | // Parse a pseudo-URL config string: 14 | // [community "@"] Host 15 | func ParseConfig(options Options, clientURL string) (Config, error) { 16 | var config = Config{ 17 | Options: options, 18 | } 19 | 20 | if parseURL, err := url.Parse("udp+snmp://" + clientURL); err != nil { 21 | return config, err 22 | } else { 23 | return config, config.parseURL(parseURL) 24 | } 25 | } 26 | 27 | func (config *Config) parseURL(configURL *url.URL) error { 28 | if configURL.User != nil { 29 | config.Community = configURL.User.Username() 30 | } 31 | 32 | config.Address = configURL.Host 33 | 34 | if configURL.Path != "" { 35 | config.Object = configURL.Path[1:] 36 | } else { 37 | config.Object = "" 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (config Config) String() string { 44 | str := "" 45 | 46 | if config.Community != "" { 47 | str += config.Community + "@" 48 | } 49 | 50 | str += config.Address 51 | 52 | if config.Object != "" { 53 | str += "/" + config.Object 54 | } 55 | 56 | return str 57 | } 58 | -------------------------------------------------------------------------------- /client/engine.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/go-logging" 6 | "io" 7 | "math/rand" 8 | "sync/atomic" 9 | ) 10 | 11 | type requestIDPool uint32 12 | 13 | func randomizedRequestIDPool() requestIDPool { 14 | return requestIDPool(rand.Uint32()) 15 | } 16 | 17 | // Return next request ID between 0..214783647 18 | func (pool *requestIDPool) atomicNext() requestID { 19 | return requestID(atomic.AddUint32((*uint32)(pool), 1) % requestIDWrapping) 20 | } 21 | 22 | func NewUDPEngine(udpOptions UDPOptions) (*Engine, error) { 23 | if udp, err := NewUDP(udpOptions); err != nil { 24 | return nil, err 25 | } else { 26 | var engine = makeEngine(udp) 27 | 28 | engine.log = logging.WithPrefix(log, fmt.Sprintf("Engine<%v>", &engine)) 29 | 30 | return &engine, nil 31 | } 32 | } 33 | 34 | func makeEngine(transport Transport) Engine { 35 | return Engine{ 36 | transport: transport, 37 | 38 | requestIDPool: randomizedRequestIDPool(), 39 | requests: make(requestMap), 40 | requestChan: make(chan *Request), 41 | timeoutChan: make(chan ioKey), 42 | recvChan: make(chan IO), 43 | closeChan: make(chan struct{}), 44 | closedChan: make(chan struct{}), 45 | } 46 | } 47 | 48 | type Engine struct { 49 | log logging.PrefixLogging 50 | transport Transport 51 | 52 | requestIDPool requestIDPool 53 | requests requestMap 54 | requestChan chan *Request 55 | timeoutChan chan ioKey 56 | 57 | recvChan chan IO 58 | recvErr error 59 | 60 | closeChan chan struct{} 61 | closedChan chan struct{} 62 | } 63 | 64 | func (engine *Engine) String() string { 65 | return fmt.Sprintf("%v", engine.transport) 66 | } 67 | 68 | // atomic, goroutine-safe 69 | func (engine *Engine) nextRequestID() requestID { 70 | return engine.requestIDPool.atomicNext() 71 | } 72 | 73 | func (engine *Engine) teardown() { 74 | engine.log.Debugf("teardown...") 75 | 76 | close(engine.requestChan) 77 | 78 | // cancel any queued requests 79 | for request := range engine.requestChan { 80 | request.close() 81 | } 82 | 83 | // cancel any active requests 84 | for _, request := range engine.requests { 85 | request.close() 86 | } 87 | 88 | // close transport 89 | if err := engine.transport.Close(); err != nil { 90 | engine.log.Warnf("SNMP<%v> close failed: %v", engine.transport, err) 91 | } else { 92 | // flush recv to let goroutine complete 93 | for range engine.recvChan { 94 | 95 | } 96 | } 97 | 98 | close(engine.closedChan) 99 | } 100 | 101 | func (engine *Engine) receiver() { 102 | defer close(engine.recvChan) 103 | 104 | for { 105 | if recv, err := engine.transport.Recv(); err != nil { 106 | if protocolErr, ok := err.(ProtocolError); ok { 107 | engine.log.Warnf("Recv: %v", protocolErr) 108 | 109 | continue 110 | } else if err == io.EOF { 111 | engine.log.Debugf("Recv: %v", err) 112 | 113 | engine.recvErr = nil 114 | 115 | return 116 | } else { 117 | engine.log.Errorf("Recv: %v", err) 118 | 119 | engine.recvErr = err 120 | 121 | return 122 | } 123 | } else { 124 | engine.log.Debugf("Recv: %#v", recv) 125 | 126 | engine.recvChan <- recv 127 | } 128 | } 129 | } 130 | 131 | func (engine *Engine) sendRequest(request *Request) error { 132 | engine.log.Debugf("Send: %#v", request.send) 133 | 134 | if err := engine.transport.Send(request.send); err != nil { 135 | return fmt.Errorf("SNMP<%v> send failed: %v", engine.transport, err) 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (engine *Engine) startRequest(request *Request) { 142 | // initialize request with next request ID to get the request key used to track send/recv/timeout 143 | requestKey := request.init(engine.nextRequestID()) 144 | 145 | if _, exists := engine.requests[requestKey]; exists { 146 | engine.log.Warnf("Start request %v allocated a duplicate requestKey: %v", request, requestKey) 147 | 148 | request.fail(fmt.Errorf("Request ID collision: %v", requestKey)) 149 | 150 | } else if err := engine.sendRequest(request); err != nil { 151 | engine.log.Debugf("Start request %v failed: %v", requestKey, err) 152 | 153 | request.fail(err) 154 | } else { 155 | engine.log.Debugf("Start request %v: %v", requestKey, request) 156 | 157 | engine.requests[requestKey] = request 158 | 159 | request.startTimeout(engine.timeoutChan, requestKey) 160 | } 161 | } 162 | 163 | func (engine *Engine) recvRequest(recv IO) { 164 | requestKey := recv.key() 165 | 166 | if request, ok := engine.requests[requestKey]; !ok { 167 | engine.log.Warnf("Unknown request %v recv", requestKey) 168 | } else { 169 | engine.log.Debugf("Request %v done: %v", requestKey, request) 170 | 171 | request.done(recv) 172 | 173 | delete(engine.requests, requestKey) 174 | } 175 | } 176 | 177 | func (engine *Engine) timeoutRequest(requestKey ioKey) { 178 | if request, ok := engine.requests[requestKey]; !ok { 179 | engine.log.Warnf("Unknown request %v timeout", requestKey) 180 | 181 | } else if request.retry <= 0 { 182 | engine.log.Debugf("Timeout %v request: %v", requestKey, request) 183 | 184 | request.failTimeout(engine.transport) 185 | 186 | delete(engine.requests, requestKey) 187 | 188 | } else { 189 | request.retry-- 190 | 191 | engine.log.Debugf("Retry request %v on timeout (%d attempts remaining): %v", requestKey, request.retry, request) 192 | 193 | if err := engine.sendRequest(request); err != nil { 194 | engine.log.Debugf("Retry request %v failed: %v", requestKey, err) 195 | 196 | // cleanup 197 | delete(engine.requests, requestKey) 198 | 199 | request.fail(err) 200 | } else { 201 | request.startTimeout(engine.timeoutChan, requestKey) 202 | } 203 | } 204 | } 205 | 206 | func (engine *Engine) run() error { 207 | defer engine.teardown() 208 | 209 | for { 210 | select { 211 | case request := <-engine.requestChan: 212 | engine.startRequest(request) 213 | 214 | case recvIO, ok := <-engine.recvChan: 215 | if !ok { 216 | return engine.recvErr 217 | } 218 | 219 | engine.recvRequest(recvIO) 220 | 221 | case requestKey := <-engine.timeoutChan: 222 | engine.timeoutRequest(requestKey) 223 | 224 | case <-engine.closeChan: 225 | return nil 226 | } 227 | } 228 | } 229 | 230 | func (engine *Engine) Transport() Transport { 231 | return engine.transport 232 | } 233 | 234 | func (engine *Engine) Run() error { 235 | engine.log.Debugf("Run...") 236 | 237 | go engine.receiver() 238 | 239 | return engine.run() 240 | } 241 | 242 | // Send request, wait for timeout or response 243 | // Returns error if send failed, request aborted on engine close, or request timeout 244 | // Also check request.Response() for SNMP-level errors 245 | func (engine *Engine) Request(request *Request) error { 246 | engine.requestChan <- request 247 | 248 | return request.wait() 249 | } 250 | 251 | func (engine *Engine) close() { 252 | close(engine.closeChan) 253 | } 254 | func (engine *Engine) waitClosed() { 255 | <-engine.closedChan 256 | } 257 | 258 | // Closing the engine will cancel any requests, and cause Run() to return 259 | // Waits for the engine goroutines to stop, and returns any same error as Run() 260 | func (engine *Engine) Close() error { 261 | engine.log.Debugf("Close...") 262 | 263 | engine.close() 264 | engine.waitClosed() 265 | 266 | return engine.recvErr 267 | } 268 | -------------------------------------------------------------------------------- /client/logging.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/qmsk/go-logging" 5 | ) 6 | 7 | var log logging.Logging 8 | 9 | func SetLogging(l logging.Logging) { 10 | log = l 11 | } 12 | -------------------------------------------------------------------------------- /client/options.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "flag" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "time" 7 | ) 8 | 9 | const ( 10 | SNMPVersion = snmp.SNMPv2c 11 | DefaultTimeout = 1 * time.Second 12 | DefaultRetry = uint(3) 13 | DefaultMaxVars = uint(50) 14 | DefaultMaxRepetitions = uint(20) 15 | ) 16 | 17 | type Options struct { 18 | Community string 19 | Timeout time.Duration 20 | Retry uint 21 | UDP UDPOptions 22 | MaxVars uint 23 | MaxRepetitions uint 24 | NoBulk bool 25 | } 26 | 27 | func (options *Options) InitFlags() { 28 | flag.StringVar(&options.Community, "snmp-community", "public", "Default SNMP community") 29 | flag.DurationVar(&options.Timeout, "snmp-timeout", DefaultTimeout, "SNMP request timeout") 30 | flag.UintVar(&options.Retry, "snmp-retry", DefaultRetry, "SNMP request retry") 31 | flag.UintVar(&options.UDP.Size, "snmp-udp-size", UDPSize, "Maximum UDP recv size") 32 | flag.UintVar(&options.MaxVars, "snmp-maxvars", DefaultMaxVars, "Maximum request VarBinds") 33 | flag.UintVar(&options.MaxRepetitions, "snmp-maxrepetitions", DefaultMaxRepetitions, "Maximum repetitions for GetBulk") 34 | flag.BoolVar(&options.NoBulk, "snmp-nobulk", false, "Do not use GetBulk requests") 35 | } 36 | -------------------------------------------------------------------------------- /client/request.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "time" 7 | ) 8 | 9 | /* 10 | PDU ::= SEQUENCE { 11 | request-id INTEGER (-214783648..214783647), 12 | */ 13 | type requestID int32 14 | 15 | const requestIDWrapping = (1 << 31) 16 | 17 | type requestMap map[ioKey]*Request 18 | 19 | // send.Addr must be resolved using engine.Transport().Resolve(...) 20 | func NewRequest(options Options, send IO) *Request { 21 | var request = makeRequest() 22 | 23 | request.send = send 24 | request.timeout = DefaultTimeout 25 | request.retry = DefaultRetry 26 | request.startTime = time.Now() 27 | 28 | if options.Timeout != 0 { 29 | request.timeout = options.Timeout 30 | } 31 | if options.Retry != 0 { 32 | request.retry = options.Retry 33 | } 34 | 35 | return &request 36 | } 37 | 38 | func makeRequest() Request { 39 | return Request{ 40 | waitChan: make(chan error, 1), 41 | } 42 | } 43 | 44 | type Request struct { 45 | send IO 46 | id requestID 47 | timeout time.Duration 48 | retry uint 49 | startTime time.Time 50 | timer *time.Timer 51 | waitChan chan error 52 | recv IO 53 | recvOK bool 54 | } 55 | 56 | func (request Request) String() string { 57 | if request.recvOK { 58 | return fmt.Sprintf("%v<%v>@%v[%d] => %v", request.send.PDUType, request.send.PDU, request.send.Addr, request.id, request.recv.PDU) 59 | } else { 60 | return fmt.Sprintf("%v<%v>@%v[%d]", request.send.PDUType, request.send.PDU, request.send.Addr, request.id) 61 | } 62 | } 63 | 64 | // Return any SNMPError, or nil 65 | func (request *Request) error() error { 66 | if pduError := request.recv.PDU.GetError(); pduError.ErrorStatus != 0 { 67 | return SNMPError{ 68 | RequestType: request.send.PDUType, 69 | ResponseType: request.recv.PDUType, 70 | ResponseError: pduError, 71 | } 72 | } else { 73 | return nil 74 | } 75 | } 76 | 77 | func (request *Request) Result() (IO, error) { 78 | if !request.recvOK { 79 | return request.recv, fmt.Errorf("Request is not done") 80 | } else if err := request.error(); err != nil { 81 | return request.recv, err 82 | } else { 83 | return request.recv, nil 84 | } 85 | } 86 | 87 | func (request *Request) wait() error { 88 | if err, ok := <-request.waitChan; !ok { 89 | return fmt.Errorf("request canceled") 90 | } else { 91 | return err 92 | } 93 | } 94 | 95 | func (request *Request) init(id requestID) ioKey { 96 | request.id = id 97 | request.send.RequestID = int(id) 98 | 99 | return request.send.key() 100 | } 101 | 102 | func (request *Request) startTimeout(timeoutChan chan ioKey, key ioKey) { 103 | request.timer = time.AfterFunc(request.timeout, func() { 104 | timeoutChan <- key 105 | }) 106 | } 107 | 108 | func (request *Request) close() { 109 | if request.timer != nil { 110 | request.timer.Stop() 111 | } 112 | close(request.waitChan) 113 | } 114 | 115 | func (request *Request) fail(err error) { 116 | request.waitChan <- err 117 | request.close() 118 | } 119 | 120 | func (request *Request) done(recv IO) { 121 | request.recv = recv 122 | request.recvOK = true 123 | request.waitChan <- nil 124 | request.close() 125 | } 126 | 127 | func (request *Request) failTimeout(transport Transport) { 128 | request.fail(TimeoutError{ 129 | transport: transport, 130 | request: request, 131 | Duration: time.Now().Sub(request.startTime), 132 | }) 133 | } 134 | 135 | type TimeoutError struct { 136 | transport Transport 137 | request *Request 138 | Duration time.Duration 139 | } 140 | 141 | func (err TimeoutError) Error() string { 142 | return fmt.Sprintf("SNMP<%v> timeout for %v after %v", err.transport, err.request, err.Duration) 143 | } 144 | 145 | type SNMPError struct { 146 | RequestType snmp.PDUType 147 | ResponseType snmp.PDUType 148 | ResponseError snmp.PDUError 149 | } 150 | 151 | func (err SNMPError) Error() string { 152 | return fmt.Sprintf("SNMP %v error: %v @ %v", err.RequestType, err.ResponseError.ErrorStatus, err.ResponseError.VarBind) 153 | } 154 | -------------------------------------------------------------------------------- /client/transport.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "io" 7 | "net" 8 | ) 9 | 10 | var EOF = io.EOF 11 | 12 | // key used to match send/recv IOs by the Engine 13 | type ioKey struct { 14 | id int 15 | community string 16 | addr string 17 | } 18 | 19 | func (key ioKey) String() string { 20 | return fmt.Sprintf("%v@%v[%d]", key.community, key.addr, key.id) 21 | } 22 | 23 | type IO struct { 24 | Addr net.Addr 25 | Packet snmp.Packet 26 | snmp.PDUMeta 27 | PDU snmp.PDU 28 | } 29 | 30 | func (io IO) key() ioKey { 31 | return ioKey{ 32 | id: io.RequestID, 33 | community: string(io.Packet.Community), 34 | addr: io.Addr.String(), 35 | } 36 | } 37 | 38 | type Transport interface { 39 | Resolve(addr string) (net.Addr, error) 40 | 41 | // Returns ProtocolError in case of soft failures 42 | Send(IO) error 43 | 44 | // Returns ProtocolError in case of soft failures 45 | Recv() (IO, error) 46 | Close() error 47 | } 48 | 49 | // soft application-layer errors, transport itself is still working 50 | type ProtocolError struct { 51 | err error 52 | } 53 | 54 | func (err ProtocolError) Error() string { 55 | return err.err.Error() 56 | } 57 | -------------------------------------------------------------------------------- /client/transport_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/snmp" 5 | "github.com/stretchr/testify/mock" 6 | "net" 7 | ) 8 | 9 | type testAddr string 10 | 11 | func (addr testAddr) Network() string { 12 | return "test" 13 | } 14 | 15 | func (addr testAddr) String() string { 16 | return string(addr) 17 | } 18 | 19 | func makeTestTransport() testTransport { 20 | return testTransport{ 21 | recvChan: make(chan IO), 22 | } 23 | } 24 | 25 | type testTransport struct { 26 | mock.Mock 27 | 28 | recvChan chan IO 29 | recvErrorChan chan error 30 | 31 | passRequestID bool 32 | } 33 | 34 | func (transport *testTransport) String() string { 35 | return "testing" 36 | } 37 | 38 | func (transport *testTransport) Resolve(addr string) (net.Addr, error) { 39 | return testAddr(addr), nil 40 | } 41 | 42 | func (transport *testTransport) Send(io IO) error { 43 | var requestID = io.RequestID 44 | 45 | if !transport.passRequestID { 46 | // override requestID to 0 for assert.Equal() comparison 47 | io.RequestID = 0 48 | } 49 | 50 | args := transport.MethodCalled(io.PDUType.String(), io) 51 | 52 | if ret := args.Get(1); ret == nil { 53 | // no response 54 | } else { 55 | recv := ret.(IO) 56 | 57 | if !transport.passRequestID { 58 | recv.RequestID = requestID 59 | } 60 | 61 | transport.recvChan <- recv 62 | } 63 | 64 | return args.Error(0) 65 | } 66 | 67 | func (transport *testTransport) Recv() (IO, error) { 68 | select { 69 | case io, ok := <-transport.recvChan: 70 | if ok { 71 | return io, nil 72 | } else { 73 | return io, EOF 74 | } 75 | case err := <-transport.recvErrorChan: 76 | return IO{}, err 77 | } 78 | } 79 | 80 | func (transport *testTransport) Close() error { 81 | close(transport.recvChan) 82 | 83 | return nil 84 | } 85 | 86 | func (transport *testTransport) mockGetTimeout(addr string, oid snmp.OID) { 87 | transport.On("GetRequest", IO{ 88 | Addr: testAddr(addr), 89 | Packet: snmp.Packet{ 90 | Version: snmp.SNMPv2c, 91 | Community: []byte("public"), 92 | }, 93 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetRequestType}, 94 | PDU: snmp.GenericPDU{ 95 | VarBinds: []snmp.VarBind{ 96 | snmp.MakeVarBind(oid, nil), 97 | }, 98 | }, 99 | }).Return(error(nil), nil) 100 | } 101 | 102 | func (transport *testTransport) mockGet(addr string, oid snmp.OID, varBind snmp.VarBind) { 103 | transport.On("GetRequest", IO{ 104 | Addr: testAddr(addr), 105 | Packet: snmp.Packet{ 106 | Version: snmp.SNMPv2c, 107 | Community: []byte("public"), 108 | }, 109 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetRequestType}, 110 | PDU: snmp.GenericPDU{ 111 | VarBinds: []snmp.VarBind{ 112 | snmp.MakeVarBind(oid, nil), 113 | }, 114 | }, 115 | }).Return(error(nil), IO{ 116 | Addr: testAddr(addr), 117 | Packet: snmp.Packet{ 118 | Version: snmp.SNMPv2c, 119 | Community: []byte("public"), 120 | }, 121 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetResponseType}, 122 | PDU: snmp.GenericPDU{ 123 | VarBinds: []snmp.VarBind{ 124 | varBind, 125 | }, 126 | }, 127 | }) 128 | } 129 | 130 | func (transport *testTransport) mockGetMany(addr string, oids []snmp.OID, varBinds []snmp.VarBind) { 131 | var reqVars = make([]snmp.VarBind, len(oids)) 132 | for i, oid := range oids { 133 | reqVars[i] = snmp.MakeVarBind(oid, nil) 134 | } 135 | 136 | transport.On("GetRequest", IO{ 137 | Addr: testAddr(addr), 138 | Packet: snmp.Packet{ 139 | Version: snmp.SNMPv2c, 140 | Community: []byte("public"), 141 | }, 142 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetRequestType}, 143 | PDU: snmp.GenericPDU{ 144 | VarBinds: reqVars, 145 | }, 146 | }).Return(error(nil), IO{ 147 | Addr: testAddr(addr), 148 | Packet: snmp.Packet{ 149 | Version: snmp.SNMPv2c, 150 | Community: []byte("public"), 151 | }, 152 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetResponseType}, 153 | PDU: snmp.GenericPDU{ 154 | VarBinds: varBinds, 155 | }, 156 | }) 157 | } 158 | 159 | func (transport *testTransport) mockGetNext(addr string, oid snmp.OID, varBind snmp.VarBind) { 160 | transport.On("GetNextRequest", IO{ 161 | Addr: testAddr(addr), 162 | Packet: snmp.Packet{ 163 | Version: snmp.SNMPv2c, 164 | Community: []byte("public"), 165 | }, 166 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetNextRequestType}, 167 | PDU: snmp.GenericPDU{ 168 | VarBinds: []snmp.VarBind{ 169 | snmp.MakeVarBind(oid, nil), 170 | }, 171 | }, 172 | }).Return(error(nil), IO{ 173 | Addr: testAddr(addr), 174 | Packet: snmp.Packet{ 175 | Version: snmp.SNMPv2c, 176 | Community: []byte("public"), 177 | }, 178 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetResponseType}, 179 | PDU: snmp.GenericPDU{ 180 | VarBinds: []snmp.VarBind{ 181 | varBind, 182 | }, 183 | }, 184 | }) 185 | } 186 | 187 | func (transport *testTransport) mockGetNextMulti(addr string, oids []snmp.OID, varBinds []snmp.VarBind) { 188 | var requestVars = make([]snmp.VarBind, len(oids)) 189 | for i, oid := range oids { 190 | requestVars[i] = snmp.MakeVarBind(oid, nil) 191 | } 192 | 193 | transport.On("GetNextRequest", IO{ 194 | Addr: testAddr(addr), 195 | Packet: snmp.Packet{ 196 | Version: snmp.SNMPv2c, 197 | Community: []byte("public"), 198 | }, 199 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetNextRequestType}, 200 | PDU: snmp.GenericPDU{ 201 | VarBinds: requestVars, 202 | }, 203 | }).Return(error(nil), IO{ 204 | Addr: testAddr(addr), 205 | Packet: snmp.Packet{ 206 | Version: snmp.SNMPv2c, 207 | Community: []byte("public"), 208 | }, 209 | PDUMeta: snmp.PDUMeta{PDUType: snmp.GetResponseType}, 210 | PDU: snmp.GenericPDU{ 211 | VarBinds: varBinds, 212 | }, 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /client/udp.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "syscall" 8 | ) 9 | 10 | const UDPPort = "161" 11 | const UDPSize uint = 64 * 1024 12 | 13 | type UDPOptions struct { 14 | Size uint 15 | } 16 | 17 | func makeUDP(options UDPOptions) UDP { 18 | if options.Size == 0 { 19 | options.Size = UDPSize 20 | } 21 | 22 | return UDP{ 23 | size: options.Size, 24 | } 25 | } 26 | 27 | func resolveUDP(addr string) (*net.UDPAddr, error) { 28 | if _, port, _ := net.SplitHostPort(addr); port == "" { 29 | addr = net.JoinHostPort(addr, UDPPort) 30 | } 31 | 32 | return net.ResolveUDPAddr("udp", addr) 33 | } 34 | 35 | func NewUDP(options UDPOptions) (*UDP, error) { 36 | var udp = makeUDP(options) 37 | 38 | if udpConn, err := net.ListenUDP("udp", &net.UDPAddr{}); err != nil { 39 | return nil, err 40 | } else { 41 | udp.conn = udpConn 42 | } 43 | 44 | if udpAddr, err := udp.LocalAddr(); err != nil { 45 | return nil, err 46 | } else { 47 | udp.addr = udpAddr 48 | } 49 | 50 | return &udp, nil 51 | } 52 | 53 | func ListenUDP(addr string, options UDPOptions) (*UDP, error) { 54 | var udp = makeUDP(options) 55 | 56 | if udpAddr, err := resolveUDP(addr); err != nil { 57 | return nil, err 58 | } else if udpConn, err := net.ListenUDP("udp", udpAddr); err != nil { 59 | return nil, err 60 | } else { 61 | udp.addr = udpAddr 62 | udp.conn = udpConn 63 | } 64 | 65 | return &udp, nil 66 | } 67 | 68 | func DialUDP(addr string, options UDPOptions) (*UDP, error) { 69 | var udp = makeUDP(options) 70 | 71 | if udpAddr, err := resolveUDP(addr); err != nil { 72 | return nil, err 73 | } else if udpConn, err := net.DialUDP("udp", nil, udpAddr); err != nil { 74 | return nil, err 75 | } else { 76 | udp.addr = udpAddr 77 | udp.conn = udpConn 78 | } 79 | 80 | return &udp, nil 81 | } 82 | 83 | type UDP struct { 84 | size uint 85 | addr *net.UDPAddr 86 | conn *net.UDPConn 87 | } 88 | 89 | func (udp *UDP) String() string { 90 | return fmt.Sprintf("%v", udp.addr) 91 | } 92 | 93 | func (udp *UDP) LocalAddr() (*net.UDPAddr, error) { 94 | switch localAddr := udp.conn.LocalAddr().(type) { 95 | case *net.UDPAddr: 96 | return localAddr, nil 97 | default: 98 | return nil, fmt.Errorf("Unknown LocalAddr type %T", localAddr) 99 | } 100 | } 101 | 102 | func (udp *UDP) Resolve(addr string) (net.Addr, error) { 103 | return resolveUDP(addr) 104 | } 105 | 106 | func (udp *UDP) send(buf []byte, addr net.Addr) error { 107 | var size int 108 | var err error 109 | 110 | if addr == nil { 111 | size, err = udp.conn.Write(buf) 112 | } else { 113 | size, err = udp.conn.WriteTo(buf, addr) 114 | } 115 | 116 | if err != nil { 117 | return err 118 | } else if size != len(buf) { 119 | return fmt.Errorf("short write: %d < %d", size, len(buf)) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func (udp *UDP) Send(send IO) error { 126 | if err := send.Packet.PackPDU(send.PDUMeta, send.PDU); err != nil { 127 | return ProtocolError{fmt.Errorf("packet.PackPDU: %v", err)} 128 | } else if buf, err := send.Packet.Marshal(); err != nil { 129 | return ProtocolError{fmt.Errorf("packet.Marshal: %v", err)} 130 | } else if err := udp.send(buf, send.Addr); err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (udp *UDP) Recv() (recv IO, err error) { 138 | var buf = make([]byte, udp.size) 139 | 140 | // recv 141 | if size, _, flags, addr, err := udp.conn.ReadMsgUDP(buf, nil); err != nil { 142 | return recv, err 143 | } else if size == 0 { 144 | return recv, io.EOF 145 | } else if flags&syscall.MSG_TRUNC != 0 { 146 | return recv, ProtocolError{fmt.Errorf("Packet truncated (>%d bytes)", udp.size)} 147 | } else { 148 | recv.Addr = addr 149 | buf = buf[:size] 150 | } 151 | 152 | if err := recv.Packet.Unmarshal(buf); err != nil { 153 | return recv, ProtocolError{fmt.Errorf("packet.Unmarshal: %v", err)} 154 | } 155 | 156 | if pduMeta, pdu, err := recv.Packet.UnpackPDU(); err != nil { 157 | return recv, ProtocolError{fmt.Errorf("packet.UnpackPDU: %v", err)} 158 | } else { 159 | recv.PDUMeta = pduMeta 160 | recv.PDU = pdu 161 | } 162 | 163 | return recv, nil 164 | } 165 | 166 | func (udp *UDP) Close() error { 167 | return udp.conn.Close() 168 | } 169 | -------------------------------------------------------------------------------- /client/udp_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "net" 7 | ) 8 | 9 | func makeTestServer() *testServer { 10 | var testServer = testServer{ 11 | values: make(map[string]interface{}), 12 | } 13 | 14 | if udp, err := ListenUDP("127.0.0.1:0", UDPOptions{}); err != nil { 15 | panic(err) 16 | } else { 17 | testServer.udp = udp 18 | } 19 | 20 | if udpAddr, err := testServer.udp.LocalAddr(); err != nil { 21 | panic(err) 22 | } else { 23 | testServer.udpAddr = udpAddr 24 | } 25 | 26 | return &testServer 27 | } 28 | 29 | type testServer struct { 30 | udp *UDP 31 | udpAddr *net.UDPAddr 32 | 33 | values map[string]interface{} 34 | } 35 | 36 | func (testServer *testServer) MockGet(oid snmp.OID, value interface{}) { 37 | testServer.values[oid.String()] = value 38 | } 39 | 40 | func (testServer *testServer) get(oid snmp.OID) (snmp.VarBind, error) { 41 | var varBind = snmp.MakeVarBind(oid, nil) 42 | 43 | if value, ok := testServer.values[oid.String()]; ok { 44 | varBind.Set(value) 45 | } else { 46 | varBind.SetError(snmp.NoSuchObjectValue) 47 | } 48 | 49 | return varBind, nil 50 | } 51 | 52 | func (testServer *testServer) handleGet(pdu snmp.GenericPDU) (snmp.PDU, error) { 53 | var response = snmp.GenericPDU{ 54 | RequestID: pdu.RequestID, 55 | VarBinds: make([]snmp.VarBind, len(pdu.VarBinds)), 56 | } 57 | 58 | for i, get := range pdu.VarBinds { 59 | if varBind, err := testServer.get(get.OID()); err == nil { 60 | response.VarBinds[i] = varBind 61 | } else if errorStatus, ok := err.(snmp.ErrorStatus); ok { 62 | response.ErrorStatus = errorStatus 63 | response.ErrorIndex = i 64 | response.VarBinds[i] = get 65 | } else { 66 | return response, err 67 | } 68 | } 69 | 70 | return response, nil 71 | } 72 | 73 | func (testServer *testServer) handle(recv IO) (send IO, err error) { 74 | send.Addr = recv.Addr 75 | send.Packet.Version = recv.Packet.Version 76 | send.Packet.Community = recv.Packet.Community 77 | 78 | switch recv.PDUType { 79 | case snmp.GetRequestType: 80 | send.PDUType = snmp.GetResponseType 81 | send.PDU, err = testServer.handleGet(recv.PDU.(snmp.GenericPDU)) 82 | default: 83 | return send, fmt.Errorf("Invalid request PDU type: %v", recv.PDUType) 84 | } 85 | 86 | return send, nil 87 | } 88 | 89 | func (testServer *testServer) run() { 90 | for { 91 | if recv, err := testServer.udp.Recv(); err != nil { 92 | panic(err) 93 | } else if send, err := testServer.handle(recv); err != nil { 94 | panic(err) 95 | } else if err := testServer.udp.Send(send); err != nil { 96 | panic(err) 97 | } 98 | } 99 | } 100 | 101 | func (testServer *testServer) stop() { 102 | testServer.udp.conn.Close() 103 | } 104 | -------------------------------------------------------------------------------- /cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/qmsk/go-logging" 7 | "github.com/qmsk/snmpbot/client" 8 | "github.com/qmsk/snmpbot/mibs" 9 | "github.com/qmsk/snmpbot/snmp" 10 | "log" 11 | "os" 12 | ) 13 | 14 | type Options struct { 15 | Logging logging.Options 16 | MIBs mibs.Options 17 | MIBsLogging logging.Options 18 | Client client.Options 19 | ClientLogging logging.Options 20 | } 21 | 22 | func (options *Options) InitFlags() { 23 | options.MIBsLogging = logging.Options{ 24 | Module: "mibs", 25 | Defaults: &options.Logging, 26 | } 27 | options.ClientLogging = logging.Options{ 28 | Module: "client", 29 | Defaults: &options.Logging, 30 | } 31 | 32 | options.Logging.InitFlags() 33 | options.MIBsLogging.InitFlags() 34 | options.ClientLogging.InitFlags() 35 | 36 | options.MIBs.InitFlags() 37 | options.Client.InitFlags() 38 | } 39 | 40 | func (options *Options) Parse() []string { 41 | flag.Parse() 42 | 43 | SetLogging(options.Logging.MakeLogging()) 44 | mibs.SetLogging(options.MIBsLogging.MakeLogging()) 45 | client.SetLogging(options.ClientLogging.MakeLogging()) 46 | 47 | return flag.Args() 48 | } 49 | 50 | func (options Options) ClientEngine() (*client.Engine, error) { 51 | return client.NewUDPEngine(options.Client.UDP) 52 | } 53 | 54 | func (options Options) ClientConfig(url string) (client.Config, error) { 55 | return client.ParseConfig(options.Client, url) 56 | } 57 | 58 | func (options Options) ParseClientIDs(engine *client.Engine, args []string) (*client.Client, []mibs.ID, error) { 59 | if len(args) < 1 { 60 | return nil, nil, fmt.Errorf("Usage: [options] ") 61 | } 62 | 63 | if clientConfig, err := options.ClientConfig(args[0]); err != nil { 64 | return nil, nil, fmt.Errorf("Invalid addr %v: %v", args[0], err) 65 | } else if ids, err := options.ResolveIDs(args[1:]); err != nil { 66 | return nil, nil, err 67 | } else if client, err := client.NewClient(engine, clientConfig); err != nil { 68 | return nil, nil, fmt.Errorf("NewClient: %v", err) 69 | } else { 70 | return client, ids, nil 71 | } 72 | } 73 | 74 | func (options Options) runEngine(engine *client.Engine) { 75 | if err := engine.Run(); err != nil { 76 | log.Fatalf("FATAL client:Engine.Run: %v", err) 77 | } else { 78 | log.Fatalf("FATAL client:Engine.Run: stopped") 79 | } 80 | } 81 | 82 | func (options Options) withEngine(engine *client.Engine, f func() error) error { 83 | go options.runEngine(engine) 84 | defer engine.Close() 85 | 86 | return f() 87 | } 88 | 89 | func (options Options) WithEngine(args []string, f func(*client.Engine) error) error { 90 | if engine, err := options.ClientEngine(); err != nil { 91 | return err 92 | } else { 93 | return options.withEngine(engine, func() error { 94 | return f(engine) 95 | }) 96 | } 97 | } 98 | 99 | func (options Options) WithClientOIDs(args []string, f func(*client.Client, ...snmp.OID) error) error { 100 | if engine, err := options.ClientEngine(); err != nil { 101 | return err 102 | } else if client, ids, err := options.ParseClientIDs(engine, args); err != nil { 103 | return err 104 | } else { 105 | var oids = make([]snmp.OID, len(ids)) 106 | 107 | for i, id := range ids { 108 | oids[i] = id.OID 109 | } 110 | 111 | return options.withEngine(engine, func() error { 112 | return f(client, oids...) 113 | }) 114 | } 115 | } 116 | 117 | func (options Options) WithClientIDs(args []string, f func(mibs.Client, ...mibs.ID) error) error { 118 | if engine, err := options.ClientEngine(); err != nil { 119 | return err 120 | } else if snmpClient, ids, err := options.ParseClientIDs(engine, args); err != nil { 121 | return err 122 | } else { 123 | var client = mibs.MakeClient(snmpClient) 124 | 125 | return options.withEngine(engine, func() error { 126 | return f(client, ids...) 127 | }) 128 | } 129 | } 130 | 131 | func (options Options) WithClientID(args []string, f func(mibs.Client, mibs.ID) error) error { 132 | if engine, err := options.ClientEngine(); err != nil { 133 | return err 134 | } else if snmpClient, ids, err := options.ParseClientIDs(engine, args); err != nil { 135 | return err 136 | } else { 137 | var client = mibs.MakeClient(snmpClient) 138 | 139 | return options.withEngine(engine, func() error { 140 | for _, id := range ids { 141 | if err := f(client, id); err != nil { 142 | return err 143 | } 144 | } 145 | 146 | return nil 147 | }) 148 | } 149 | } 150 | 151 | func (options *Options) Main(f func(args []string) error) { 152 | args := options.Parse() 153 | 154 | if err := options.MIBs.LoadMIBs(); err != nil { 155 | log.Fatal(err) 156 | os.Exit(1) 157 | } 158 | 159 | if err := f(args); err != nil { 160 | log.Fatal(err) 161 | os.Exit(1) 162 | } 163 | 164 | os.Exit(0) 165 | } 166 | -------------------------------------------------------------------------------- /cmd/logging.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/qmsk/go-logging" 5 | ) 6 | 7 | var Log logging.Logging // public for cmd packages 8 | 9 | func SetLogging(l logging.Logging) { 10 | Log = l 11 | } 12 | -------------------------------------------------------------------------------- /cmd/mibs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/mibs" 6 | _ "github.com/qmsk/snmpbot/mibs/bridge_mib" 7 | "github.com/qmsk/snmpbot/snmp" 8 | "log" 9 | ) 10 | 11 | func ParseOID(arg string) (snmp.OID, error) { 12 | return mibs.ParseOID(arg) 13 | } 14 | 15 | func (options Options) ResolveID(name string) (mibs.ID, error) { 16 | return mibs.Resolve(name) 17 | } 18 | 19 | func (options Options) ResolveIDs(names []string) ([]mibs.ID, error) { 20 | var ids = make([]mibs.ID, len(names)) 21 | 22 | for i, name := range names { 23 | if id, err := options.ResolveID(name); err != nil { 24 | return nil, fmt.Errorf("Invalid ID %v: %v", name, err) 25 | } else { 26 | ids[i] = id 27 | } 28 | } 29 | 30 | return ids, nil 31 | } 32 | 33 | func (options Options) FormatOID(oid snmp.OID) string { 34 | return mibs.FormatOID(oid) 35 | } 36 | 37 | func (options Options) PrintVarBind(varBind snmp.VarBind) { 38 | if object := mibs.LookupObject(varBind.OID()); object != nil { 39 | options.PrintObject(object, varBind) 40 | } else if value, err := varBind.Value(); err != nil { 41 | log.Printf("VarBind[%v]: %v", varBind.OID(), err) 42 | } else { 43 | fmt.Printf("%v = <%T> %v\n", options.FormatOID(varBind.OID()), value, value) 44 | } 45 | } 46 | 47 | func (options Options) PrintObject(object *mibs.Object, varBind snmp.VarBind) { 48 | if name, value, err := object.Format(varBind); err != nil { 49 | log.Printf("VarBind[%v](%v): %v", varBind.OID(), object, err) 50 | } else { 51 | fmt.Printf("%v = %v\n", name, value) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/snmpbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/qmsk/go-logging" 7 | "github.com/qmsk/go-web" 8 | "github.com/qmsk/snmpbot/client" 9 | "github.com/qmsk/snmpbot/cmd" 10 | "github.com/qmsk/snmpbot/server" 11 | ) 12 | 13 | type Options struct { 14 | cmd.Options 15 | 16 | Server server.Options 17 | ServerLogging logging.Options 18 | Web web.Options 19 | WebLogging logging.Options 20 | } 21 | 22 | func (options *Options) InitFlags() { 23 | options.ServerLogging = logging.Options{ 24 | Module: "server", 25 | Defaults: &options.Options.Logging, 26 | } 27 | options.WebLogging = logging.Options{ 28 | Module: "web", 29 | Defaults: &options.Options.Logging, 30 | } 31 | options.Options.InitFlags() 32 | options.Server.InitFlags() 33 | options.ServerLogging.InitFlags() 34 | options.WebLogging.InitFlags() 35 | 36 | flag.StringVar(&options.Web.Listen, "http-listen", ":8286", "HTTP server listen: [HOST]:PORT") 37 | flag.StringVar(&options.Web.Static, "http-static", "", "HTTP sever /static path: PATH") 38 | } 39 | 40 | func (options *Options) Apply() { 41 | server.SetLogging(options.ServerLogging.MakeLogging()) 42 | web.SetLogging(options.WebLogging.MakeLogging()) 43 | } 44 | 45 | var options Options 46 | 47 | func init() { 48 | options.InitFlags() 49 | } 50 | 51 | func run(serverEngine server.Engine) error { 52 | // XXX: this is not a good API, it just returns immediately if there is no -http-listen? 53 | options.Web.Server( 54 | options.Web.RouteAPI("/api/", server.WebAPI(serverEngine)), 55 | options.Web.RouteStatic("/"), 56 | ) 57 | 58 | return nil 59 | } 60 | 61 | func main() { 62 | options.Main(func(args []string) error { 63 | options.Apply() 64 | 65 | return options.WithEngine(args, func(engine *client.Engine) error { 66 | if config, err := options.Server.LoadConfig(options.Client); err != nil { 67 | return fmt.Errorf("Failed to load server config: %v", err) 68 | } else if serverEngine, err := options.Server.Engine(engine, config); err != nil { 69 | return fmt.Errorf("Failed to load server: %v", err) 70 | } else { 71 | return run(serverEngine) 72 | } 73 | }) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/snmpget/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/client" 6 | "github.com/qmsk/snmpbot/cmd" 7 | "github.com/qmsk/snmpbot/snmp" 8 | ) 9 | 10 | type Options struct { 11 | cmd.Options 12 | } 13 | 14 | var options Options 15 | 16 | func init() { 17 | options.InitFlags() 18 | } 19 | 20 | func snmpget(client *client.Client, oids ...snmp.OID) error { 21 | if varBinds, err := client.Get(oids...); err != nil { 22 | return fmt.Errorf("client.Get: %v", err) 23 | } else { 24 | for _, varBind := range varBinds { 25 | options.PrintVarBind(varBind) 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func main() { 33 | options.Main(func(args []string) error { 34 | return options.WithClientOIDs(args, snmpget) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/snmpobject/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/cmd" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "log" 8 | ) 9 | 10 | type Options struct { 11 | cmd.Options 12 | } 13 | 14 | var options Options 15 | 16 | func init() { 17 | options.InitFlags() 18 | } 19 | 20 | func snmpobject(client mibs.Client, id mibs.ID) error { 21 | var object = id.Object() 22 | 23 | if object == nil { 24 | return fmt.Errorf("Not an object: %v", id) 25 | } 26 | 27 | return client.WalkObjects([]*mibs.Object{object}, func(object *mibs.Object, indexValues mibs.IndexValues, value mibs.Value, err error) error { 28 | if err != nil { 29 | log.Printf("%v: %v", object, err) 30 | } else { 31 | fmt.Printf("%v%v = %v\n", object, object.IndexSyntax.FormatValues(indexValues), value) 32 | } 33 | 34 | return nil 35 | }) 36 | } 37 | 38 | func main() { 39 | options.Main(func(args []string) error { 40 | return options.WithClientID(args, snmpobject) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/snmpprobe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/cmd" 6 | "github.com/qmsk/snmpbot/mibs" 7 | ) 8 | 9 | type Options struct { 10 | cmd.Options 11 | } 12 | 13 | var options Options 14 | 15 | func init() { 16 | options.InitFlags() 17 | } 18 | 19 | func snmpprobe(client mibs.Client, ids ...mibs.ID) error { 20 | if len(ids) == 0 { 21 | mibs.WalkMIBs(func(mib *mibs.MIB) { 22 | ids = append(ids, mib.ID) 23 | }) 24 | } 25 | 26 | if probed, err := client.Probe(ids); err != nil { 27 | return err 28 | } else { 29 | for i, ok := range probed { 30 | fmt.Printf("%v = %v\n", ids[i], ok) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func main() { 38 | options.Main(func(args []string) error { 39 | return options.WithClientIDs(args, snmpprobe) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/snmptable/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/cmd" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "os" 8 | "text/tabwriter" 9 | ) 10 | 11 | type Options struct { 12 | cmd.Options 13 | } 14 | 15 | var options Options 16 | 17 | func init() { 18 | options.InitFlags() 19 | } 20 | 21 | func snmptable(client mibs.Client, id mibs.ID) error { 22 | var table = id.Table() 23 | var writer = tabwriter.NewWriter(os.Stdout, 8, 4, 1, ' ', 0) 24 | 25 | if table == nil { 26 | return fmt.Errorf("Not a table: %v", id) 27 | } 28 | 29 | for _, indexObject := range table.IndexSyntax { 30 | fmt.Fprintf(writer, "%v\t", indexObject) 31 | } 32 | fmt.Fprintf(writer, "|") 33 | for _, entryObject := range table.EntrySyntax { 34 | fmt.Fprintf(writer, "\t%v", entryObject) 35 | } 36 | fmt.Fprintf(writer, "\n") 37 | 38 | for range table.IndexSyntax { 39 | fmt.Fprintf(writer, "---\t") 40 | } 41 | fmt.Fprintf(writer, "|") 42 | for range table.EntrySyntax { 43 | fmt.Fprintf(writer, "\t---") 44 | } 45 | fmt.Fprintf(writer, "\n") 46 | 47 | walkRow := func(indexValues mibs.IndexValues, entryValues mibs.EntryValues, err error) error { 48 | if err != nil { 49 | cmd.Log.Warnf("%v", err) 50 | } 51 | 52 | for i, _ := range table.IndexSyntax { 53 | fmt.Fprintf(writer, "%v\t", indexValues[i]) 54 | } 55 | fmt.Fprintf(writer, "|") 56 | for i, _ := range table.EntrySyntax { 57 | fmt.Fprintf(writer, "\t%v", entryValues[i]) 58 | } 59 | fmt.Fprintf(writer, "\n") 60 | 61 | return nil 62 | } 63 | 64 | if err := client.WalkTable(table, walkRow); err != nil { 65 | return err 66 | } 67 | 68 | writer.Flush() 69 | 70 | return nil 71 | } 72 | 73 | func main() { 74 | options.Main(func(args []string) error { 75 | return options.WithClientID(args, snmptable) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/snmpwalk/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/client" 5 | "github.com/qmsk/snmpbot/cmd" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type Options struct { 10 | cmd.Options 11 | } 12 | 13 | var options Options 14 | 15 | func init() { 16 | options.InitFlags() 17 | } 18 | 19 | func snmpwalk(client *client.Client, oids ...snmp.OID) error { 20 | return client.WalkObjects(oids, func(varBinds []snmp.VarBind) error { 21 | for _, varBind := range varBinds { 22 | options.PrintVarBind(varBind) 23 | } 24 | 25 | return nil 26 | }) 27 | } 28 | 29 | func main() { 30 | options.Main(func(args []string) error { 31 | return options.WithClientOIDs(args, snmpwalk) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qmsk/snmpbot 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/geoffgarside/ber v0.0.0-20181018193237-27a1aff36ce6 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | github.com/qmsk/go-logging v0.2.0 11 | github.com/qmsk/go-web v0.3.2-0.20200822195857-4a8e50c643dd 12 | github.com/stretchr/objx v0.1.1 // indirect 13 | github.com/stretchr/testify v1.6.1 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/geoffgarside/ber v0.0.0-20181018193237-27a1aff36ce6 h1:oQvud7S1g8tBMy3xj0dW1v4sCsrAPGPoiyajFYi8zQU= 7 | github.com/geoffgarside/ber v0.0.0-20181018193237-27a1aff36ce6/go.mod h1:x6zPZPDIQQKmaIDbeEzUGnxSmj7raqK6G8m6jkTlgbU= 8 | github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= 9 | github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 10 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= 11 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/qmsk/go-logging v0.2.0 h1:OxN6cz9K4pqk/6gF3SUabUKCOIrNMu7b08hbjMQ7/oU= 15 | github.com/qmsk/go-logging v0.2.0/go.mod h1:H7uaeU7oLrdZEbfEAt/FVk81P8kioPlv67UnGJTvnFI= 16 | github.com/qmsk/go-web v0.3.0 h1:Nhlr8gFecsY/+zB9tvLxzep9ghiKWr0k4iuUW4Qdvgk= 17 | github.com/qmsk/go-web v0.3.0/go.mod h1:N2t5t+tn2pDpi5uK7Gvn8w5Cnk+zeK0MG5udFqzU9ng= 18 | github.com/qmsk/go-web v0.3.2-0.20200822173957-5155e677e7aa h1:gTJc/sFtJ/XM7xN2DPDajBba6LfoxyPi/jYtbEkNj9I= 19 | github.com/qmsk/go-web v0.3.2-0.20200822173957-5155e677e7aa/go.mod h1:L89hUxvXF3oL+lU2WzS5ng3eize3HoeBqC0y/BKIxeU= 20 | github.com/qmsk/go-web v0.3.2-0.20200822195857-4a8e50c643dd h1:9qACBdFJrqiZIZSzMs0+amMFOvMh5q3u1/gf8jzgaTo= 21 | github.com/qmsk/go-web v0.3.2-0.20200822195857-4a8e50c643dd/go.mod h1:L89hUxvXF3oL+lU2WzS5ng3eize3HoeBqC0y/BKIxeU= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 24 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 25 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 26 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 27 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 28 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 31 | golang.org/x/net v0.0.0-20181017193950-04a2e542c03f h1:4pRM7zYwpBjCnfA1jRmhItLxYJkaEnsmuAcRtA347DA= 32 | golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 33 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 34 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 35 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /mibs/bridge_mib/syntax_bridge_id.go: -------------------------------------------------------------------------------- 1 | package bridge_mib 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/mibs" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type BridgeID struct { 10 | Priority uint 11 | mibs.MACAddress 12 | } 13 | 14 | func (value BridgeID) String() string { 15 | return fmt.Sprintf("%d@%v", value.Priority, value.MACAddress) 16 | } 17 | 18 | type BridgeIDSyntax struct{} 19 | 20 | func (syntax BridgeIDSyntax) UnpackIndex(index []int) (mibs.Value, []int, error) { 21 | // TODO 22 | return nil, index, mibs.SyntaxIndexError{syntax, index} 23 | } 24 | 25 | func (syntax BridgeIDSyntax) Unpack(varBind snmp.VarBind) (mibs.Value, error) { 26 | snmpValue, err := varBind.Value() 27 | if err != nil { 28 | return nil, err 29 | } 30 | switch value := snmpValue.(type) { 31 | case []byte: 32 | var bridgeID BridgeID 33 | 34 | if len(value) != 8 { 35 | return nil, mibs.SyntaxError{syntax, value} 36 | } else { 37 | bridgeID.Priority = uint(value[0])<<8 + uint(value[1]) 38 | copy(bridgeID.MACAddress[:], value[2:8]) 39 | } 40 | return bridgeID, nil 41 | default: 42 | return nil, mibs.SyntaxError{syntax, value} 43 | } 44 | } 45 | 46 | func init() { 47 | mibs.RegisterSyntax("BRIDGE-MIB::BridgeId", BridgeIDSyntax{}) 48 | } 49 | -------------------------------------------------------------------------------- /mibs/bridge_mib/syntax_port_list.go: -------------------------------------------------------------------------------- 1 | package bridge_mib 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "github.com/qmsk/snmpbot/snmp" 8 | ) 9 | 10 | type PortList []uint8 11 | 12 | func (value PortList) List() []uint { 13 | var ports []uint 14 | 15 | for byteOffset, octet := range value { 16 | var bitOffset uint 17 | for bitOffset = 0; bitOffset < 8; bitOffset++ { 18 | var port = uint(byteOffset)*8 + bitOffset + 1 19 | var bit = octet&(1<<(8-bitOffset-1)) != 0 20 | 21 | if bit { 22 | ports = append(ports, port) 23 | } 24 | } 25 | } 26 | 27 | return ports 28 | } 29 | 30 | func (value PortList) Map() map[uint]bool { 31 | var ports = make(map[uint]bool) 32 | 33 | for byteOffset, octet := range value { 34 | var bitOffset uint 35 | for bitOffset = 0; bitOffset < 8; bitOffset++ { 36 | var port = uint(byteOffset)*8 + bitOffset + 1 37 | var bit = octet&(1<<(8-bitOffset-1)) != 0 38 | 39 | ports[port] = bit 40 | } 41 | } 42 | 43 | return ports 44 | } 45 | 46 | func (value PortList) String() string { 47 | return fmt.Sprintf("%v", value.List()) 48 | } 49 | 50 | func (value PortList) MarshalJSON() ([]byte, error) { 51 | return json.Marshal(value.List()) 52 | } 53 | 54 | type PortListSyntax struct{} 55 | 56 | func (syntax PortListSyntax) UnpackIndex(index []int) (mibs.Value, []int, error) { 57 | return nil, index, mibs.SyntaxIndexError{syntax, index} 58 | } 59 | 60 | func (syntax PortListSyntax) Unpack(varBind snmp.VarBind) (mibs.Value, error) { 61 | snmpValue, err := varBind.Value() 62 | if err != nil { 63 | return nil, err 64 | } 65 | switch value := snmpValue.(type) { 66 | case []byte: 67 | return PortList(value), nil 68 | default: 69 | return nil, mibs.SyntaxError{syntax, value} 70 | } 71 | } 72 | 73 | func init() { 74 | mibs.RegisterSyntax("Q-BRIDGE-MIB::PortList", PortListSyntax{}) 75 | } 76 | -------------------------------------------------------------------------------- /mibs/bridge_mib/syntax_stp_port_id.go: -------------------------------------------------------------------------------- 1 | package bridge_mib 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "github.com/qmsk/snmpbot/snmp" 8 | ) 9 | 10 | type PortID struct { 11 | Priority uint 12 | Index uint 13 | } 14 | 15 | func (value PortID) String() string { 16 | return fmt.Sprintf("%d.%d", value.Priority, value.Index) 17 | } 18 | 19 | func (value PortID) MarshalJSON() ([]byte, error) { 20 | return json.Marshal(value.String()) 21 | } 22 | 23 | type PortIDSyntax struct{} 24 | 25 | func (syntax PortIDSyntax) UnpackIndex(index []int) (mibs.Value, []int, error) { 26 | // TODO 27 | return nil, index, mibs.SyntaxIndexError{syntax, index} 28 | } 29 | 30 | func (syntax PortIDSyntax) Unpack(varBind snmp.VarBind) (mibs.Value, error) { 31 | snmpValue, err := varBind.Value() 32 | if err != nil { 33 | return nil, err 34 | } 35 | switch value := snmpValue.(type) { 36 | case []byte: 37 | var portID PortID 38 | 39 | if len(value) != 2 { 40 | return nil, mibs.SyntaxError{syntax, value} 41 | } else { 42 | var uintValue uint16 = uint16(value[0])<<8 + uint16(value[1]) 43 | 44 | portID.Priority = uint((uintValue & 0xf000) >> 8) // effectively * 16 45 | portID.Index = uint(uintValue & 0x0fff) 46 | } 47 | return portID, nil 48 | default: 49 | return nil, mibs.SyntaxError{syntax, value} 50 | } 51 | } 52 | 53 | func init() { 54 | // XXX: This is made up for BRIDGE-MIB::dot1dStpPortDesignatedPort 55 | mibs.RegisterSyntax("BRIDGE-MIB::PortId", PortIDSyntax{}) 56 | } 57 | -------------------------------------------------------------------------------- /mibs/client.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/client" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | func MakeClient(c *client.Client) Client { 9 | return Client{Client: c} 10 | } 11 | 12 | type Client struct { 13 | *client.Client 14 | } 15 | 16 | // Probe the MIB at id 17 | func (client Client) Probe(ids []ID) ([]bool, error) { 18 | var oids = make([]snmp.OID, len(ids)) 19 | var probed = make([]bool, len(ids)) 20 | 21 | for i, id := range ids { 22 | oids[i] = id.OID 23 | } 24 | 25 | if varBinds, err := client.GetScalars(oids); err != nil { 26 | return probed, err 27 | } else { 28 | for i, varBind := range varBinds { 29 | if err := varBind.ErrorValue(); err != nil { 30 | // not supported 31 | } else { 32 | probed[i] = true 33 | } 34 | } 35 | } 36 | 37 | return probed, nil 38 | } 39 | 40 | func (client Client) WalkObjects(objects []*Object, f func(*Object, IndexValues, Value, error) error) error { 41 | var oids = make([]snmp.OID, len(objects)) 42 | 43 | for i, object := range objects { 44 | oids[i] = object.OID 45 | } 46 | 47 | return client.Client.WalkObjects(oids, func(varBinds []snmp.VarBind) error { 48 | for i, varBind := range varBinds { 49 | var object = objects[i] 50 | var walkErr error 51 | 52 | if err := varBind.ErrorValue(); err != nil { 53 | // just skip unsupported objects... 54 | } else if value, err := object.Unpack(varBind); err != nil { 55 | walkErr = f(object, nil, value, err) 56 | } else if indexValues, err := object.UnpackIndex(varBind.OID()); err != nil { 57 | walkErr = f(object, indexValues, value, err) 58 | } else { 59 | walkErr = f(object, indexValues, value, nil) 60 | } 61 | 62 | if walkErr != nil { 63 | return walkErr 64 | } 65 | } 66 | 67 | return nil 68 | }) 69 | } 70 | 71 | func (client Client) WalkTable(table *Table, f func(IndexValues, EntryValues, error) error) error { 72 | return client.Client.WalkTable(table.EntryOIDs(), func(varBinds []snmp.VarBind) error { 73 | indexValues, entryValues, err := table.Unpack(varBinds) 74 | 75 | return f(indexValues, entryValues, err) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /mibs/config_test.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func init() { 11 | if err := Load("test/TEST2-MIB.json"); err != nil { 12 | panic(err) 13 | } 14 | } 15 | 16 | func TestConfigResolveMIB(t *testing.T) { 17 | if resolveMIB, err := ResolveMIB("TEST2-MIB"); err != nil { 18 | t.Errorf("ResolveMIB TEST2-MIB: %v", err) 19 | } else { 20 | assert.Equal(t, "TEST2-MIB", resolveMIB.String()) 21 | } 22 | } 23 | 24 | func TestConfigResolve(t *testing.T) { 25 | if id, err := Resolve("TEST2-MIB"); err != nil { 26 | t.Errorf("Resolve TEST2-MIB: %v", err) 27 | } else { 28 | assert.Equal(t, "TEST2-MIB", id.String()) 29 | } 30 | } 31 | 32 | func TestConfigResolveObjectID(t *testing.T) { 33 | if id, err := Resolve("TEST2-MIB::test"); err != nil { 34 | t.Errorf("Resolve TEST2-MIB::test: %v", err) 35 | } else { 36 | assert.Equal(t, "TEST2-MIB::test", id.String()) 37 | } 38 | } 39 | 40 | func TestConfigResolveTableID(t *testing.T) { 41 | if id, err := Resolve("TEST2-MIB::testTable"); err != nil { 42 | t.Errorf("Resolve TEST2-MIB::testTable: %v", err) 43 | } else { 44 | assert.Equal(t, "TEST2-MIB::testTable", id.String()) 45 | } 46 | } 47 | 48 | func TestConfigResolveObject(t *testing.T) { 49 | if object, err := ResolveObject("TEST2-MIB::test"); err != nil { 50 | t.Errorf("ResolveObject TEST2-MIB::test: %v", err) 51 | } else { 52 | assert.Equal(t, "TEST2-MIB::test", object.String()) 53 | assert.Equal(t, &DisplayStringSyntax{}, object.Syntax) 54 | } 55 | } 56 | 57 | func TestConfigResolveTable(t *testing.T) { 58 | mib := LookupMIB(snmp.OID{1, 0, 2}) 59 | 60 | if table, err := ResolveTable("TEST2-MIB::testTable"); err != nil { 61 | t.Errorf("ResolveTable TEST2-MIB::testTable: %v", err) 62 | } else { 63 | assert.Equal(t, "TEST2-MIB::testTable", table.String()) 64 | assert.Equal(t, IndexSyntax{mib.ResolveObject("testID")}, table.IndexSyntax) 65 | assert.Equal(t, EntrySyntax{mib.ResolveObject("testName")}, table.EntrySyntax) 66 | } 67 | } 68 | 69 | func TestConfigObjectIndexSyntax(t *testing.T) { 70 | mib := LookupMIB(snmp.OID{1, 0, 2}) 71 | 72 | if object, err := ResolveObject("TEST2-MIB::testName"); err != nil { 73 | t.Errorf("ResolveObject TEST2-MIB::testName: %v", err) 74 | } else { 75 | assert.Equal(t, "TEST2-MIB::testName", object.String()) 76 | assert.Equal(t, &DisplayStringSyntax{}, object.Syntax) 77 | assert.Equal(t, IndexSyntax{mib.ResolveObject("testID")}, object.IndexSyntax) 78 | 79 | var varBind = snmp.MakeVarBind(object.OID.Extend(10), []byte("foobar")) 80 | 81 | if name, value, err := object.Format(varBind); err != nil { 82 | t.Errorf("Object<%v>.Format %v: %v", object, varBind, err) 83 | } else { 84 | assert.Equal(t, "TEST2-MIB::testName[10]", name) 85 | assert.Equal(t, "foobar", fmt.Sprintf("%v", value)) 86 | } 87 | } 88 | } 89 | 90 | func TestConfigObjectEnumSyntax(t *testing.T) { 91 | if object, err := ResolveObject("TEST2-MIB::testEnum"); err != nil { 92 | t.Errorf("ResolveObject TEST2-MIB::testEnum: %v", err) 93 | } else { 94 | assert.Equal(t, "TEST2-MIB::testEnum", object.String()) 95 | assert.Equal(t, &EnumSyntax{ 96 | {1, "one"}, 97 | {2, "two"}, 98 | }, object.Syntax) 99 | 100 | var varBind = snmp.MakeVarBind(object.OID.Extend(0), int(1)) 101 | 102 | if name, value, err := object.Format(varBind); err != nil { 103 | t.Errorf("Object<%v>.Format %v: %v", object, varBind, err) 104 | } else { 105 | assert.Equal(t, "TEST2-MIB::testEnum", name) 106 | assert.Equal(t, "one", fmt.Sprintf("%v", value)) 107 | } 108 | } 109 | } 110 | 111 | func TestConfigResolveIDExternal(t *testing.T) { 112 | if object, err := ResolveObject("TEST2-MIB::extObject"); err != nil { 113 | t.Errorf("ResolveObject TEST2-MIB::extObject: %v", err) 114 | } else { 115 | assert.Equal(t, "TEST2-MIB::extObject", object.String()) 116 | } 117 | } 118 | 119 | func TestConfigLookupObject(t *testing.T) { 120 | if object := LookupObject(snmp.OID{1, 0, 2, 1, 1}); object == nil { 121 | t.Errorf("LookupObject .1.0.2.1.1: %v", nil) 122 | } else { 123 | assert.Equal(t, "TEST2-MIB::test", object.String()) 124 | 125 | var varBind = snmp.MakeVarBind(object.OID.Extend(0), []byte("foobar")) 126 | 127 | if name, value, err := object.Format(varBind); err != nil { 128 | t.Errorf("Object<%v>.Format %v: %v", object, varBind, err) 129 | } else { 130 | assert.Equal(t, "TEST2-MIB::test", name) 131 | assert.Equal(t, "foobar", fmt.Sprintf("%v", value)) 132 | } 133 | } 134 | } 135 | 136 | func TestConfigLookupObjectExt(t *testing.T) { 137 | if object := LookupObject(snmp.OID{1, 1, 5, 1}); object == nil { 138 | t.Errorf("LookupObject .1.1.5.1: %v", nil) 139 | } else { 140 | assert.Equal(t, "TEST2-MIB::extObject", object.String()) 141 | } 142 | } 143 | 144 | func TestConfigObjectUnknownSyntax(t *testing.T) { 145 | if object, err := ResolveObject("TEST2-MIB::testUnknownSyntax"); err != nil { 146 | t.Errorf("ResolveObject TEST2-MIB::testUnknownSyntax: %v", err) 147 | } else { 148 | assert.Equal(t, "TEST2-MIB::testUnknownSyntax", object.String()) 149 | assert.Equal(t, nil, object.Syntax) 150 | 151 | var varBind = snmp.MakeVarBind(object.OID.Extend(0), int(1)) 152 | 153 | if name, value, err := object.Format(varBind); err != nil { 154 | t.Errorf("Object<%v>.Format %v: %v", object, varBind, err) 155 | } else { 156 | assert.Equal(t, "TEST2-MIB::testUnknownSyntax", name) 157 | assert.Equal(t, "1", fmt.Sprintf("%v", value)) 158 | } 159 | } 160 | } 161 | 162 | func TestConfigResolveTableAugments(t *testing.T) { 163 | mib := LookupMIB(snmp.OID{1, 0, 2}) 164 | 165 | if table, err := ResolveTable("TEST2-MIB::testTable2"); err != nil { 166 | t.Errorf("ResolveTable TEST2-MIB::testTable2: %v", err) 167 | } else { 168 | assert.Equal(t, "TEST2-MIB::testTable2", table.String()) 169 | assert.Equal(t, IndexSyntax{mib.ResolveObject("testID")}, table.IndexSyntax) 170 | assert.Equal(t, EntrySyntax{mib.ResolveObject("testName2")}, table.EntrySyntax) 171 | } 172 | } 173 | 174 | func TestConfigObjectAugmentsIndexSyntax(t *testing.T) { 175 | mib := LookupMIB(snmp.OID{1, 0, 2}) 176 | 177 | if object, err := ResolveObject("TEST2-MIB::testName2"); err != nil { 178 | t.Errorf("ResolveObject TEST2-MIB::testName2: %v", err) 179 | } else { 180 | assert.Equal(t, "TEST2-MIB::testName2", object.String()) 181 | assert.Equal(t, &DisplayStringSyntax{}, object.Syntax) 182 | assert.Equal(t, IndexSyntax{mib.ResolveObject("testID")}, object.IndexSyntax) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /mibs/id.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type IDKey string 9 | 10 | type ID struct { 11 | MIB *MIB 12 | Name string 13 | OID snmp.OID 14 | } 15 | 16 | func (id ID) Key() IDKey { 17 | return IDKey(id.OID.String()) // TODO: perf? 18 | } 19 | 20 | func (id ID) String() string { 21 | if id.MIB == nil { 22 | return id.OID.String() 23 | } else if id.Name == "" { 24 | return id.MIB.FormatOID(id.OID) 25 | } else { 26 | return fmt.Sprintf("%s::%s", id.MIB.Name, id.Name) 27 | } 28 | } 29 | 30 | func (id ID) MakeID(name string, ids ...int) ID { 31 | return ID{id.MIB, name, id.OID.Extend(ids...)} 32 | } 33 | 34 | func (id ID) FormatOID(oid snmp.OID) string { 35 | if index := id.OID.Index(oid); index == nil { 36 | return oid.String() 37 | } else if len(index) == 0 { 38 | return id.String() 39 | } else if id.Name == "" { 40 | return fmt.Sprintf("%s%s", id.MIB.Name, snmp.OID(index).String()) 41 | } else { 42 | return fmt.Sprintf("%s::%s%s", id.MIB.Name, id.Name, snmp.OID(index).String()) 43 | } 44 | } 45 | 46 | func (id ID) Object() *Object { 47 | if id.MIB == nil { 48 | return nil 49 | } else { 50 | return id.MIB.Object(id) 51 | } 52 | } 53 | 54 | func (id ID) Table() *Table { 55 | if id.MIB == nil { 56 | return nil 57 | } else { 58 | return id.MIB.Table(id) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mibs/id_test.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/snmp" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | type idTest struct { 10 | str string 11 | id ID 12 | err string 13 | } 14 | 15 | func testResolve(t *testing.T, test idTest) { 16 | id, err := Resolve(test.str) 17 | 18 | if test.err != "" { 19 | assert.EqualErrorf(t, err, test.err, "Resolve(%#v)", test.str) 20 | } else if err != nil { 21 | t.Errorf("Resolve(%#v): %v", test.str, err) 22 | } else { 23 | assert.Equal(t, test.id, id, "Resolve(%#v)", test.str) 24 | } 25 | } 26 | 27 | func testIDString(t *testing.T, test idTest) { 28 | str := test.id.String() 29 | 30 | assert.Equal(t, test.str, str, "%#v.String()", test.id) 31 | } 32 | 33 | func testID(t *testing.T, test idTest) { 34 | testResolve(t, test) 35 | testIDString(t, test) 36 | } 37 | 38 | func TestResolveInvalidSyntax(t *testing.T) { 39 | testResolve(t, idTest{ 40 | str: ":x", 41 | err: "Invalid syntax: :x", 42 | }) 43 | } 44 | 45 | func TestResolveInvalidMIB(t *testing.T) { 46 | testResolve(t, idTest{ 47 | str: "::foo", 48 | err: "Invalid name without MIB: ::foo", 49 | }) 50 | } 51 | 52 | func TestResolveMIBNotFoundError(t *testing.T) { 53 | testResolve(t, idTest{ 54 | str: "ASDF-MIB", 55 | err: "MIB not found: ASDF-MIB", 56 | }) 57 | } 58 | 59 | func TestResolveNameNotFoundError(t *testing.T) { 60 | testResolve(t, idTest{ 61 | str: "TEST-MIB::missing", 62 | err: "TEST-MIB name not found: missing", 63 | }) 64 | } 65 | 66 | func TestResolveInvalidName(t *testing.T) { 67 | testResolve(t, idTest{ 68 | str: "TEST-MIB::.0", 69 | err: "Invalid syntax: TEST-MIB::.0", 70 | }) 71 | } 72 | 73 | func TestResolveInvalidIndex(t *testing.T) { 74 | testResolve(t, idTest{ 75 | str: "TEST-MIB.0..0", 76 | err: "Invalid OID part: ", 77 | }) 78 | } 79 | 80 | func TestIDMIB(t *testing.T) { 81 | testID(t, idTest{ 82 | str: "TEST-MIB", 83 | id: ID{MIB: TestMIB, OID: snmp.OID{1, 0, 1}}, 84 | }) 85 | } 86 | 87 | func TestIDMIBIndex(t *testing.T) { 88 | testID(t, idTest{ 89 | str: "TEST-MIB.2.1", 90 | id: ID{MIB: TestMIB, OID: snmp.OID{1, 0, 1, 2, 1}}, 91 | }) 92 | } 93 | 94 | func TestIDMIBName(t *testing.T) { 95 | testID(t, idTest{ 96 | str: "TEST-MIB::test", 97 | id: ID{MIB: TestMIB, Name: "test", OID: snmp.OID{1, 0, 1, 1, 1}}, 98 | }) 99 | } 100 | 101 | func TestResolveMIBNameIndex(t *testing.T) { 102 | testResolve(t, idTest{ 103 | str: "TEST-MIB::test.0", 104 | id: ID{MIB: TestMIB, Name: "test", OID: snmp.OID{1, 0, 1, 1, 1, 0}}, 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /mibs/index.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type IndexSyntax []*Object 9 | type IndexValues []Value 10 | type IndexMap map[IDKey]Value 11 | 12 | func (indexSyntax IndexSyntax) UnpackIndex(index []int) (IndexValues, error) { 13 | if indexSyntax == nil { 14 | if len(index) == 1 && index[0] == 0 { 15 | return IndexValues{}, nil 16 | } else { 17 | return nil, fmt.Errorf("Unexpected leaf index: %v", index) 18 | } 19 | } 20 | 21 | var values = make(IndexValues, len(indexSyntax)) 22 | 23 | for i, indexObject := range indexSyntax { 24 | if indexValue, indexRemaining, err := indexObject.Syntax.UnpackIndex(index); err != nil { 25 | return values, fmt.Errorf("Invalid index for %v: %v", indexObject, err) 26 | } else { 27 | values[i] = indexValue 28 | index = indexRemaining 29 | } 30 | } 31 | 32 | if len(index) > 0 { 33 | return values, fmt.Errorf("Trailing index values: %v", index) 34 | } 35 | 36 | return values, nil 37 | } 38 | 39 | func (indexSyntax IndexSyntax) MapIndex(index []int) (IndexMap, error) { 40 | var indexMap = make(IndexMap) 41 | 42 | for _, indexObject := range indexSyntax { 43 | if indexValue, indexRemaining, err := indexObject.Syntax.UnpackIndex(index); err != nil { 44 | return nil, fmt.Errorf("Invalid index for %v: %v", indexObject, err) 45 | } else { 46 | indexMap[indexObject.ID.Key()] = indexValue 47 | index = indexRemaining 48 | } 49 | } 50 | 51 | return indexMap, nil 52 | } 53 | 54 | func (indexSyntax IndexSyntax) FormatIndex(index []int) (string, error) { 55 | if indexValues, err := indexSyntax.UnpackIndex(index); err != nil { 56 | return "", err 57 | } else { 58 | return indexSyntax.FormatValues(indexValues), nil 59 | } 60 | } 61 | 62 | func (indexSyntax IndexSyntax) FormatValues(indexValues IndexValues) string { 63 | var indexStrings = make([]string, len(indexSyntax)) 64 | 65 | for i, indexValue := range indexValues { 66 | indexStrings[i] = fmt.Sprintf("[%v]", indexValue) 67 | } 68 | 69 | return strings.Join(indexStrings, "") 70 | } 71 | -------------------------------------------------------------------------------- /mibs/logging.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/qmsk/go-logging" 5 | ) 6 | 7 | var log logging.Logging 8 | 9 | func SetLogging(l logging.Logging) { 10 | log = l 11 | } 12 | -------------------------------------------------------------------------------- /mibs/mib.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "regexp" 7 | ) 8 | 9 | func makeMIB(name string, oid snmp.OID) MIB { 10 | return MIB{ 11 | ID: ID{OID: oid}, 12 | Name: name, 13 | registry: makeRegistry(), 14 | objects: make(map[IDKey]*Object), 15 | tables: make(map[IDKey]*Table), 16 | } 17 | } 18 | 19 | type MIB struct { 20 | ID 21 | Name string // shadows ID.Name, which is empty 22 | registry 23 | 24 | objects map[IDKey]*Object 25 | tables map[IDKey]*Table 26 | } 27 | 28 | func (mib *MIB) String() string { 29 | return mib.Name 30 | } 31 | 32 | func (mib *MIB) MakeID(name string, ids ...int) ID { 33 | return ID{mib, name, mib.OID.Extend(ids...)} 34 | } 35 | 36 | func (mib *MIB) registerObject(object Object) *Object { 37 | mibRegistry.registerOID(object.ID) 38 | mib.registry.register(object.ID) 39 | mib.objects[object.ID.Key()] = &object 40 | 41 | return &object 42 | } 43 | 44 | func (mib *MIB) RegisterObject(id ID, object Object) *Object { 45 | object.ID = id 46 | 47 | return mib.registerObject(object) 48 | } 49 | 50 | func (mib *MIB) registerTable(table Table) *Table { 51 | mibRegistry.registerOID(table.ID) 52 | mib.registry.register(table.ID) 53 | mib.tables[table.ID.Key()] = &table 54 | 55 | return &table 56 | } 57 | 58 | func (mib *MIB) RegisterTable(id ID, table Table) *Table { 59 | table.ID = id 60 | 61 | return mib.registerTable(table) 62 | } 63 | 64 | /* Resolve MIB-relative ID by human-readable name: 65 | ".1.0" 66 | "sysDescr" 67 | "sysDescr.0" 68 | */ 69 | var mibResolveRegexp = regexp.MustCompile("^([^.]+?)?([.][0-9.]+)?$") 70 | 71 | func (mib *MIB) Resolve(name string) (ID, error) { 72 | var id = ID{OID: mib.OID} 73 | var nameID, nameOID string 74 | 75 | if matches := mibResolveRegexp.FindStringSubmatch(name); matches == nil { 76 | return id, fmt.Errorf("Invalid syntax: %v", name) 77 | } else { 78 | nameID = matches[1] 79 | nameOID = matches[2] 80 | } 81 | 82 | if nameID == "" { 83 | 84 | } else if resolveID, err := mib.ResolveName(nameID); err != nil { 85 | return id, err 86 | } else { 87 | id = resolveID 88 | } 89 | 90 | if nameOID == "" { 91 | 92 | } else if oid, err := snmp.ParseOID(nameOID); err != nil { 93 | return id, err 94 | } else { 95 | if id.OID == nil { 96 | id.OID = oid 97 | } else { 98 | id.OID = id.OID.Extend(oid...) 99 | } 100 | } 101 | 102 | return id, nil 103 | } 104 | 105 | func (mib *MIB) ResolveName(name string) (ID, error) { 106 | if id, ok := mib.registry.getName(name); !ok { 107 | return ID{MIB: mib, Name: name}, fmt.Errorf("%v name not found: %v", mib.Name, name) 108 | } else { 109 | return id, nil 110 | } 111 | } 112 | 113 | func (mib *MIB) Lookup(oid snmp.OID) ID { 114 | if id, ok := mib.registry.getOID(oid); !ok { 115 | return ID{MIB: mib, OID: oid} 116 | } else { 117 | return id 118 | } 119 | } 120 | 121 | func (mib *MIB) Walk(f func(ID)) { 122 | mib.registry.walk(f) 123 | } 124 | 125 | func (mib *MIB) Object(id ID) *Object { 126 | if object, ok := mib.objects[id.Key()]; !ok { 127 | return nil 128 | } else { 129 | return object 130 | } 131 | } 132 | 133 | func (mib *MIB) ResolveObject(name string) *Object { 134 | if id, err := mib.Resolve(name); err != nil { 135 | return nil 136 | } else { 137 | return mib.Object(id) 138 | } 139 | } 140 | 141 | func (mib *MIB) Table(id ID) *Table { 142 | if table, ok := mib.tables[id.Key()]; !ok { 143 | return nil 144 | } else { 145 | return table 146 | } 147 | } 148 | 149 | func (mib *MIB) ResolveTable(name string) *Table { 150 | if id, err := mib.Resolve(name); err != nil { 151 | return nil 152 | } else { 153 | return mib.Table(id) 154 | } 155 | } 156 | 157 | func (mib *MIB) FormatOID(oid snmp.OID) string { 158 | if index := mib.OID.Index(oid); index == nil { 159 | return oid.String() 160 | } else if len(index) == 0 { 161 | return mib.Name 162 | } else { 163 | return fmt.Sprintf("%s%s", mib.Name, snmp.OID(index).String()) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /mibs/mib_test.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | TestMIB = RegisterMIB("TEST-MIB", 1, 0, 1) 10 | 11 | TestObject = TestMIB.RegisterObject(TestMIB.MakeID("test", 1, 1), Object{ 12 | Syntax: DisplayStringSyntax{}, 13 | }) 14 | ) 15 | 16 | func TestResolveMIB(t *testing.T) { 17 | if mib, err := ResolveMIB("TEST-MIB"); err != nil { 18 | t.Fatalf("ResolveMIB: %v", err) 19 | } else { 20 | assert.Equal(t, TestMIB, mib) 21 | } 22 | } 23 | 24 | func TestResolveMIBError(t *testing.T) { 25 | if _, err := ResolveMIB("ASDF-MIB"); err == nil { 26 | t.Fatalf("ResolveMIB: %v", err) 27 | } else { 28 | assert.EqualError(t, err, "MIB not found: ASDF-MIB") 29 | } 30 | } 31 | 32 | func TestWalkMIBs(t *testing.T) { 33 | var found = false 34 | 35 | WalkMIBs(func(mib *MIB) { 36 | if mib == TestMIB { 37 | found = true 38 | } 39 | }) 40 | 41 | assert.True(t, found) 42 | } 43 | 44 | func TestWalkObjects(t *testing.T) { 45 | var found = false 46 | 47 | WalkObjects(func(object *Object) { 48 | if object == TestObject { 49 | found = true 50 | } 51 | }) 52 | 53 | assert.True(t, found) 54 | } 55 | 56 | func TestResolveObject(t *testing.T) { 57 | if object, err := ResolveObject("TEST-MIB::test"); err != nil { 58 | t.Fatalf("ResolveObject: %v", err) 59 | } else { 60 | assert.Equal(t, TestObject, object) 61 | } 62 | } 63 | 64 | func TestResolveObjectErrorResolve(t *testing.T) { 65 | _, err := ResolveObject("ASDF-MIB::test") 66 | 67 | assert.EqualError(t, err, "MIB not found: ASDF-MIB") 68 | } 69 | 70 | func TestResolveObjectErrorMIB(t *testing.T) { 71 | _, err := ResolveObject(".0.1") 72 | 73 | assert.EqualError(t, err, "No MIB for name: .0.1") 74 | } 75 | 76 | func TestResolveObjectErrorObject(t *testing.T) { 77 | _, err := ResolveObject("TEST-MIB.2.1") 78 | 79 | assert.EqualError(t, err, "Not an object: TEST-MIB.2.1") 80 | } 81 | 82 | func TestResolveTableErrorResolve(t *testing.T) { 83 | _, err := ResolveTable("ASDF-MIB::test") 84 | 85 | assert.EqualError(t, err, "MIB not found: ASDF-MIB") 86 | } 87 | 88 | func TestResolveTableErrorMIB(t *testing.T) { 89 | _, err := ResolveTable(".0.1") 90 | 91 | assert.EqualError(t, err, "No MIB for name: .0.1") 92 | } 93 | 94 | func TestResolveTableErrorObject(t *testing.T) { 95 | _, err := ResolveTable("TEST-MIB.2.1") 96 | 97 | assert.EqualError(t, err, "Not a table: TEST-MIB.2.1") 98 | } 99 | -------------------------------------------------------------------------------- /mibs/mibs.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "regexp" 7 | ) 8 | 9 | var mibRegistry = makeRegistry() 10 | 11 | func registerMIB(mib MIB) *MIB { 12 | mib.ID.MIB = &mib 13 | 14 | mibRegistry.registerName(mib.ID, mib.Name) 15 | 16 | if mib.OID != nil { 17 | mibRegistry.registerOID(mib.ID) 18 | } 19 | 20 | return &mib 21 | } 22 | 23 | func RegisterMIB(name string, oid ...int) *MIB { 24 | return registerMIB(makeMIB(name, snmp.OID(oid))) 25 | } 26 | 27 | func ResolveMIB(name string) (*MIB, error) { 28 | if id, ok := mibRegistry.getName(name); !ok { 29 | return nil, fmt.Errorf("MIB not found: %v", name) 30 | } else { 31 | return id.MIB, nil 32 | } 33 | } 34 | 35 | func WalkMIBs(f func(mib *MIB)) { 36 | mibRegistry.walk(func(id ID) { 37 | if id.OID == nil { 38 | // skip MIBs without a top-level OID 39 | return 40 | } 41 | 42 | f(id.MIB) 43 | }) 44 | } 45 | 46 | /* Resolve ID by human-readable name: 47 | ".1.3.6" 48 | "SNMPv2-MIB" 49 | "SNMPv2-MIB.1.0" 50 | "SNMPv2-MIB::sysDescr" 51 | "SNMPv2-MIB::sysDescr.0" 52 | */ 53 | var resolveRegexp = regexp.MustCompile("^([^.:]+?)?(?:::([^.]+?))?([.][0-9.]+)?$") 54 | 55 | func Resolve(name string) (ID, error) { 56 | var id ID 57 | var nameMIB, nameID, nameOID string 58 | 59 | if matches := resolveRegexp.FindStringSubmatch(name); matches == nil { 60 | return id, fmt.Errorf("Invalid syntax: %v", name) 61 | } else { 62 | nameMIB = matches[1] 63 | nameID = matches[2] 64 | nameOID = matches[3] 65 | } 66 | 67 | if nameMIB == "" { 68 | 69 | } else if mib, err := ResolveMIB(nameMIB); err != nil { 70 | return id, err 71 | } else { 72 | id = mib.ID 73 | id.Name = "" // fixup MIB.ID re-use of Name 74 | } 75 | 76 | if nameID == "" { 77 | 78 | } else if id.MIB == nil { 79 | return id, fmt.Errorf("Invalid name without MIB: %v", name) 80 | } else if resolveID, err := id.MIB.ResolveName(nameID); err != nil { 81 | return id, err 82 | } else { 83 | id = resolveID 84 | } 85 | 86 | if nameOID == "" { 87 | 88 | } else if oid, err := snmp.ParseOID(nameOID); err != nil { 89 | return id, err 90 | } else { 91 | if id.OID == nil { 92 | id.OID = oid 93 | } else { 94 | id.OID = id.OID.Extend(oid...) 95 | } 96 | } 97 | 98 | return id, nil 99 | } 100 | 101 | func ResolveObject(name string) (*Object, error) { 102 | if id, err := Resolve(name); err != nil { 103 | return nil, err 104 | } else if id.MIB == nil { 105 | return nil, fmt.Errorf("No MIB for name: %v", name) 106 | } else if object := id.MIB.Object(id); object == nil { 107 | return nil, fmt.Errorf("Not an object: %v", name) 108 | } else { 109 | return object, nil 110 | } 111 | } 112 | 113 | func ResolveTable(name string) (*Table, error) { 114 | if id, err := Resolve(name); err != nil { 115 | return nil, err 116 | } else if id.MIB == nil { 117 | return nil, fmt.Errorf("No MIB for name: %v", name) 118 | } else if table := id.MIB.Table(id); table == nil { 119 | return nil, fmt.Errorf("Not a table: %v", name) 120 | } else { 121 | return table, nil 122 | } 123 | } 124 | 125 | // Lookup ID by OID 126 | func LookupMIB(oid snmp.OID) *MIB { 127 | if id, ok := mibRegistry.getOID(oid); !ok { 128 | return nil 129 | } else { 130 | return id.MIB 131 | } 132 | } 133 | 134 | func Lookup(oid snmp.OID) ID { 135 | if id, ok := mibRegistry.getOID(oid); !ok { 136 | return ID{OID: oid} 137 | } else { 138 | return id 139 | } 140 | } 141 | 142 | func LookupObject(oid snmp.OID) *Object { 143 | if id := Lookup(oid); id.MIB == nil { 144 | return nil 145 | } else { 146 | return id.MIB.Object(id) 147 | } 148 | } 149 | 150 | func Walk(f func(i ID)) { 151 | mibRegistry.walk(func(id ID) { 152 | id.MIB.Walk(f) 153 | }) 154 | } 155 | 156 | func WalkObjects(f func(object *Object)) { 157 | Walk(func(id ID) { 158 | if object := id.MIB.Object(id); object != nil { 159 | f(object) 160 | } 161 | }) 162 | } 163 | 164 | func WalkTables(f func(table *Table)) { 165 | Walk(func(id ID) { 166 | if table := id.MIB.Table(id); table != nil { 167 | f(table) 168 | } 169 | }) 170 | } 171 | 172 | // Lookup human-readable object name with optional index 173 | func ParseOID(name string) (snmp.OID, error) { 174 | if id, err := Resolve(name); err != nil { 175 | return nil, err 176 | } else { 177 | return id.OID, nil 178 | } 179 | } 180 | 181 | func FormatOID(oid snmp.OID) string { 182 | return Lookup(oid).FormatOID(oid) 183 | } 184 | -------------------------------------------------------------------------------- /mibs/object.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type Object struct { 9 | ID 10 | 11 | IndexSyntax 12 | Syntax 13 | NotAccessible bool 14 | } 15 | 16 | func (object *Object) Unpack(varBind snmp.VarBind) (Value, error) { 17 | if err := varBind.ErrorValue(); err != nil { 18 | return nil, err 19 | } else if snmpValue, err := varBind.Value(); err != nil { 20 | return nil, err 21 | } else if object.Syntax == nil { 22 | return snmpValue, nil 23 | } else { 24 | // TODO: change interface to Unpack(interface{})? 25 | return object.Syntax.Unpack(varBind) 26 | } 27 | } 28 | 29 | func (object *Object) UnpackIndex(oid snmp.OID) (IndexValues, error) { 30 | if oidIndex := object.OID.Index(oid); oidIndex == nil { 31 | return nil, fmt.Errorf("Invalid OID for Object<%v>: %v", oid, object) 32 | } else { 33 | return object.IndexSyntax.UnpackIndex(oidIndex) 34 | } 35 | } 36 | 37 | func (object *Object) FormatIndex(oid snmp.OID) (string, error) { 38 | if index := object.OID.Index(oid); index == nil { 39 | return oid.String(), nil 40 | } else if len(index) == 0 { 41 | return object.String(), nil 42 | } else if indexString, err := object.IndexSyntax.FormatIndex(index); err != nil { 43 | return "", err 44 | } else { 45 | return fmt.Sprintf("%s::%s%s", object.MIB.Name, object.Name, indexString), nil 46 | } 47 | } 48 | 49 | func (object *Object) Format(varBind snmp.VarBind) (string, Value, error) { 50 | if value, err := object.Unpack(varBind); err != nil { 51 | return "", nil, err 52 | } else if name, err := object.FormatIndex(varBind.OID()); err != nil { 53 | return "", value, err 54 | } else { 55 | return name, value, err 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mibs/oid_test.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/snmp" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | type oidTest struct { 10 | name string 11 | oid snmp.OID 12 | err string 13 | } 14 | 15 | func testParseOID(t *testing.T, test oidTest) { 16 | oid, err := ParseOID(test.name) 17 | if test.err != "" { 18 | assert.EqualErrorf(t, err, test.err, "ParseOID(%#v)", test.name) 19 | } else if err != nil { 20 | t.Errorf("ParseOID(%#v): %v", test.name, err) 21 | } else { 22 | assert.Equal(t, test.oid, oid, "ParseOID(%#v)", test.name) 23 | } 24 | } 25 | 26 | func testFormatOID(t *testing.T, test oidTest) { 27 | name := FormatOID(test.oid) 28 | 29 | assert.Equal(t, test.name, name, "FormatOID(%#v)", test.oid) 30 | } 31 | 32 | func testParseFormatOID(t *testing.T, test oidTest) { 33 | testParseOID(t, test) 34 | testFormatOID(t, test) 35 | } 36 | 37 | func TestOIDError(t *testing.T) { 38 | testParseOID(t, oidTest{ 39 | name: "ASDF", 40 | err: "MIB not found: ASDF", 41 | }) 42 | } 43 | 44 | func TestOIDEmpty(t *testing.T) { 45 | testParseFormatOID(t, oidTest{ 46 | name: "", 47 | oid: nil, 48 | }) 49 | } 50 | 51 | func TestOIDRaw(t *testing.T) { 52 | testParseFormatOID(t, oidTest{ 53 | name: ".1.3.6.1", 54 | oid: snmp.OID{1, 3, 6, 1}, 55 | }) 56 | } 57 | 58 | func TestOIDMIB(t *testing.T) { 59 | testParseFormatOID(t, oidTest{ 60 | name: "TEST-MIB", 61 | oid: snmp.OID{1, 0, 1}, 62 | }) 63 | } 64 | 65 | func TestOIDMIBIndex(t *testing.T) { 66 | testParseFormatOID(t, oidTest{ 67 | name: "TEST-MIB.0", 68 | oid: snmp.OID{1, 0, 1, 0}, 69 | }) 70 | } 71 | 72 | func TestOID(t *testing.T) { 73 | testParseFormatOID(t, oidTest{ 74 | name: "TEST-MIB::test", 75 | oid: snmp.OID{1, 0, 1, 1, 1}, 76 | }) 77 | } 78 | 79 | func TestOIDIndex(t *testing.T) { 80 | testParseFormatOID(t, oidTest{ 81 | name: "TEST-MIB::test.0", 82 | oid: snmp.OID{1, 0, 1, 1, 1, 0}, 83 | }) 84 | } 85 | 86 | func TestLookupMIBNotFound(t *testing.T) { 87 | mib := LookupMIB(snmp.OID{1, 2, 0}) 88 | 89 | assert.Nil(t, mib) 90 | } 91 | 92 | func TestLookupMIB(t *testing.T) { 93 | mib := LookupMIB(snmp.OID{1, 0, 1}) 94 | 95 | assert.Equal(t, TestMIB, mib) 96 | } 97 | 98 | func TestLookupObjectNotFound(t *testing.T) { 99 | object := LookupObject(snmp.OID{1, 2, 0}) 100 | 101 | assert.Nil(t, object) 102 | } 103 | 104 | func TestLookupObjectNotObject(t *testing.T) { 105 | object := LookupObject(snmp.OID{1, 0, 1, 0, 1}) 106 | 107 | assert.Nil(t, object) 108 | } 109 | 110 | func TestLookupObject(t *testing.T) { 111 | object := LookupObject(snmp.OID{1, 0, 1, 1, 1}) 112 | 113 | assert.Equal(t, TestObject, object) 114 | } 115 | 116 | func TestLookupObjectIndex(t *testing.T) { 117 | object := LookupObject(snmp.OID{1, 0, 1, 1, 1, 0}) 118 | 119 | assert.Equal(t, TestObject, object) 120 | } 121 | -------------------------------------------------------------------------------- /mibs/options.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Options struct { 11 | MIBPath string 12 | } 13 | 14 | func (options *Options) InitFlags() { 15 | flag.StringVar(&options.MIBPath, "snmp-mibs", os.Getenv("SNMPBOT_MIBS"), "Load MIBs from PATH[:PATH[...]]") 16 | } 17 | 18 | func (options *Options) LoadMIBs() error { 19 | if options.MIBPath == "" { 20 | return fmt.Errorf("Must provide -snmp-mibs/$SNMPBOT_MIBS with path to .../snmpbot-mibs/*.json") 21 | } 22 | 23 | for _, path := range filepath.SplitList(options.MIBPath) { 24 | if err := Load(path); err != nil { 25 | return err 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /mibs/registry.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | func makeRegistry() registry { 9 | return registry{ 10 | byOID: make(map[string]ID), 11 | byName: make(map[string]ID), 12 | } 13 | } 14 | 15 | type registry struct { 16 | byOID map[string]ID 17 | byName map[string]ID 18 | } 19 | 20 | func (registry *registry) registerOID(id ID) { 21 | registry.byOID[id.OID.String()] = id 22 | } 23 | func (registry *registry) registerName(id ID, name string) { 24 | registry.byName[name] = id 25 | } 26 | 27 | func (registry *registry) register(id ID) { 28 | registry.registerOID(id) 29 | registry.registerName(id, id.Name) 30 | } 31 | 32 | func (registry *registry) getName(name string) (ID, bool) { 33 | if id, ok := registry.byName[name]; !ok { 34 | return ID{Name: name}, false 35 | } else { 36 | return id, true 37 | } 38 | } 39 | 40 | func (registry *registry) getOID(oid snmp.OID) (ID, bool) { 41 | var key = "" 42 | var id = ID{OID: oid} 43 | var ok = false 44 | 45 | for _, x := range oid { 46 | key += fmt.Sprintf(".%d", x) 47 | 48 | if getID, getOK := registry.byOID[key]; getOK { 49 | id = getID 50 | ok = true 51 | } 52 | } 53 | 54 | return id, ok 55 | } 56 | 57 | func (registry *registry) walk(f func(ID)) { 58 | for _, id := range registry.byName { 59 | f(id) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mibs/syntax.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type Value interface{} 9 | 10 | type Syntax interface { 11 | UnpackIndex([]int) (Value, []int, error) 12 | Unpack(snmp.VarBind) (Value, error) 13 | } 14 | 15 | type SyntaxError struct { 16 | Syntax Syntax 17 | SNMPValue interface{} 18 | } 19 | 20 | func (err SyntaxError) Error() string { 21 | return fmt.Sprintf("Invalid value for Syntax %T: <%T> %#v", err.Syntax, err.SNMPValue, err.SNMPValue) 22 | } 23 | 24 | type SyntaxIndexError struct { 25 | Syntax Syntax 26 | Index []int 27 | } 28 | 29 | func (err SyntaxIndexError) Error() string { 30 | return fmt.Sprintf("Invalid index for Syntax %T: %#v", err.Syntax, err.Index) 31 | } 32 | -------------------------------------------------------------------------------- /mibs/syntax_bits.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type Bit struct { 10 | Bit uint 11 | Name string 12 | } 13 | 14 | func (bit Bit) String() string { 15 | if bit.Name != "" { 16 | return bit.Name 17 | } else { 18 | return fmt.Sprintf("%d", 1<>bitOffset) != 0 { 48 | values = append(values, bit) 49 | } 50 | } 51 | 52 | return values 53 | } 54 | 55 | func (syntax BitsSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 56 | snmpValue, err := varBind.Value() 57 | if err != nil { 58 | return nil, err 59 | } 60 | switch value := snmpValue.(type) { 61 | case []uint8: 62 | return syntax.values(value), nil 63 | default: 64 | return nil, SyntaxError{syntax, value} 65 | } 66 | } 67 | 68 | func (syntax BitsSyntax) UnpackIndex(index []int) (Value, []int, error) { 69 | // TODO 70 | return nil, index, SyntaxIndexError{syntax, index} 71 | } 72 | 73 | func init() { 74 | RegisterSyntax("BITS", BitsSyntax{}) 75 | } 76 | -------------------------------------------------------------------------------- /mibs/syntax_counter.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type Counter uint 9 | 10 | func (value Counter) String() string { 11 | return fmt.Sprintf("%v", uint(value)) 12 | } 13 | 14 | type CounterSyntax struct{} 15 | 16 | func (syntax CounterSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 17 | snmpValue, err := varBind.Value() 18 | if err != nil { 19 | return nil, err 20 | } 21 | switch value := snmpValue.(type) { 22 | case snmp.Counter32: 23 | return Counter(value), nil 24 | case snmp.Counter64: 25 | return Counter(value), nil 26 | default: 27 | return nil, SyntaxError{syntax, value} 28 | } 29 | } 30 | 31 | func (syntax CounterSyntax) UnpackIndex(index []int) (Value, []int, error) { 32 | // TODO 33 | return nil, index, SyntaxIndexError{syntax, index} 34 | } 35 | 36 | func init() { 37 | RegisterSyntax("Counter", CounterSyntax{}) 38 | RegisterSyntax("Counter32", CounterSyntax{}) 39 | RegisterSyntax("Counter64", CounterSyntax{}) 40 | } 41 | -------------------------------------------------------------------------------- /mibs/syntax_displaystring.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/snmp" 5 | ) 6 | 7 | type DisplayString string 8 | 9 | type DisplayStringSyntax struct{} 10 | 11 | func (syntax DisplayStringSyntax) UnpackIndex(index []int) (Value, []int, error) { 12 | // TODO 13 | return nil, index, SyntaxIndexError{syntax, index} 14 | } 15 | 16 | func (syntax DisplayStringSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 17 | snmpValue, err := varBind.Value() 18 | if err != nil { 19 | return nil, err 20 | } 21 | switch value := snmpValue.(type) { 22 | case []byte: 23 | return DisplayString(value), nil 24 | default: 25 | return nil, SyntaxError{syntax, value} 26 | } 27 | } 28 | 29 | func init() { 30 | RegisterSyntax("SNMPv2-TC::DisplayString", DisplayStringSyntax{}) 31 | RegisterSyntax("SNMP-FRAMEWORK-MIB::SnmpAdminString", DisplayStringSyntax{}) 32 | } 33 | -------------------------------------------------------------------------------- /mibs/syntax_enum.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type Enum struct { 10 | Value int 11 | Name string 12 | } 13 | 14 | func (enum Enum) String() string { 15 | if enum.Name != "" { 16 | return enum.Name 17 | } else { 18 | return fmt.Sprintf("%d", enum.Value) 19 | } 20 | } 21 | 22 | func (enum Enum) MarshalJSON() ([]byte, error) { 23 | if enum.Name != "" { 24 | return json.Marshal(enum.Name) 25 | } else { 26 | return json.Marshal(enum.Value) 27 | } 28 | } 29 | 30 | type EnumSyntax []Enum 31 | 32 | func (syntax EnumSyntax) lookup(value int) Enum { 33 | for _, enum := range syntax { 34 | if enum.Value == value { 35 | return enum 36 | } 37 | } 38 | 39 | return Enum{Value: value} 40 | } 41 | 42 | func (syntax EnumSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 43 | snmpValue, err := varBind.Value() 44 | if err != nil { 45 | return nil, err 46 | } 47 | switch value := snmpValue.(type) { 48 | case int64: 49 | return syntax.lookup(int(value)), nil 50 | default: 51 | return nil, SyntaxError{syntax, value} 52 | } 53 | } 54 | 55 | func (syntax EnumSyntax) UnpackIndex(index []int) (Value, []int, error) { 56 | // TODO 57 | return nil, index, SyntaxIndexError{syntax, index} 58 | } 59 | 60 | func init() { 61 | RegisterSyntax("ENUM", EnumSyntax{}) 62 | } 63 | -------------------------------------------------------------------------------- /mibs/syntax_gauge.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type Gauge snmp.Gauge32 9 | 10 | func (value Gauge) String() string { 11 | return fmt.Sprintf("%v", snmp.Gauge32(value)) 12 | } 13 | 14 | type GaugeSyntax struct{} 15 | 16 | func (syntax GaugeSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 17 | snmpValue, err := varBind.Value() 18 | if err != nil { 19 | return nil, err 20 | } 21 | switch value := snmpValue.(type) { 22 | case snmp.Gauge32: 23 | return Gauge(value), nil 24 | default: 25 | return nil, SyntaxError{syntax, value} 26 | } 27 | } 28 | 29 | func (syntax GaugeSyntax) UnpackIndex(index []int) (Value, []int, error) { 30 | // TODO 31 | return nil, index, SyntaxIndexError{syntax, index} 32 | } 33 | 34 | func init() { 35 | RegisterSyntax("Gauge32", GaugeSyntax{}) 36 | } 37 | -------------------------------------------------------------------------------- /mibs/syntax_integer.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type Integer int 10 | 11 | func (value Integer) String() string { 12 | return fmt.Sprintf("%v", int(value)) 13 | } 14 | 15 | func (value Integer) MarshalJSON() ([]byte, error) { 16 | return json.Marshal(int(value)) 17 | } 18 | 19 | type IntegerSyntax struct{} 20 | 21 | func (syntax IntegerSyntax) UnpackIndex(index []int) (Value, []int, error) { 22 | if len(index) < 1 { 23 | return nil, index, SyntaxIndexError{syntax, index} 24 | } 25 | 26 | return Integer(index[0]), index[1:], nil 27 | } 28 | 29 | func (syntax IntegerSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 30 | snmpValue, err := varBind.Value() 31 | if err != nil { 32 | return nil, err 33 | } 34 | switch value := snmpValue.(type) { 35 | case int64: 36 | return Integer(value), nil 37 | default: 38 | return nil, SyntaxError{syntax, value} 39 | } 40 | } 41 | 42 | func init() { 43 | RegisterSyntax("INTEGER", IntegerSyntax{}) 44 | RegisterSyntax("Integer32", IntegerSyntax{}) 45 | } 46 | -------------------------------------------------------------------------------- /mibs/syntax_ip_address.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/qmsk/snmpbot/snmp" 6 | "net" 7 | ) 8 | 9 | type IPAddress net.IP 10 | 11 | func (value IPAddress) String() string { 12 | return net.IP(value).String() 13 | } 14 | 15 | func (value IPAddress) MarshalJSON() ([]byte, error) { 16 | return json.Marshal(value.String()) 17 | } 18 | 19 | type IPAddressSyntax struct{} 20 | 21 | func (syntax IPAddressSyntax) UnpackIndex(index []int) (Value, []int, error) { 22 | if len(index) < 4 { 23 | return nil, index, SyntaxIndexError{syntax, index} 24 | } 25 | 26 | var value = make(IPAddress, 4) 27 | 28 | for i := 0; i < 4; i++ { 29 | if index[i] < 0 || index[i] >= 256 { 30 | return nil, index, SyntaxIndexError{syntax, index[0:4]} 31 | } 32 | 33 | value[i] = byte(index[i]) 34 | } 35 | 36 | return value, index[4:], nil 37 | } 38 | 39 | func (syntax IPAddressSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 40 | snmpValue, err := varBind.Value() 41 | if err != nil { 42 | return nil, err 43 | } 44 | switch value := snmpValue.(type) { 45 | case snmp.IPAddress: 46 | var ipAddress = make(IPAddress, 4) 47 | 48 | for i := 0; i < 4; i++ { 49 | ipAddress[i] = value[i] 50 | } 51 | 52 | return ipAddress, nil 53 | default: 54 | return nil, SyntaxError{syntax, value} 55 | } 56 | } 57 | 58 | func init() { 59 | RegisterSyntax("IpAddress", IPAddressSyntax{}) 60 | } 61 | -------------------------------------------------------------------------------- /mibs/syntax_mac_address.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type MACAddress [6]byte 10 | 11 | func (value MACAddress) String() string { 12 | return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", 13 | value[0], 14 | value[1], 15 | value[2], 16 | value[3], 17 | value[4], 18 | value[5], 19 | ) 20 | } 21 | 22 | func (value MACAddress) MarshalJSON() ([]byte, error) { 23 | return json.Marshal(value.String()) 24 | } 25 | 26 | type MACAddressSyntax struct{} 27 | 28 | func (syntax MACAddressSyntax) UnpackIndex(index []int) (Value, []int, error) { 29 | if len(index) < 6 { 30 | return nil, index, SyntaxIndexError{syntax, index} 31 | } 32 | 33 | var value MACAddress 34 | 35 | for i := 0; i < 6; i++ { 36 | if index[i] < 0 || index[i] >= 256 { 37 | return nil, index, SyntaxIndexError{syntax, index[0:6]} 38 | } 39 | 40 | value[i] = byte(index[i]) 41 | } 42 | 43 | return value, index[6:], nil 44 | } 45 | 46 | func (syntax MACAddressSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 47 | snmpValue, err := varBind.Value() 48 | if err != nil { 49 | return nil, err 50 | } 51 | switch value := snmpValue.(type) { 52 | case []byte: 53 | var macAddress MACAddress 54 | 55 | if len(value) != 6 { 56 | return nil, SyntaxError{syntax, value} 57 | } else { 58 | copy(macAddress[:], value[0:6]) 59 | } 60 | return macAddress, nil 61 | default: 62 | return nil, SyntaxError{syntax, value} 63 | } 64 | } 65 | 66 | func init() { 67 | RegisterSyntax("SNMPv2-TC::MacAddress", MACAddressSyntax{}) 68 | } 69 | -------------------------------------------------------------------------------- /mibs/syntax_octet_string.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | "strings" 8 | ) 9 | 10 | type OctetString []byte 11 | 12 | func (value OctetString) String() string { 13 | var hex = make([]string, len(value)) 14 | 15 | for i, b := range value { 16 | hex[i] = fmt.Sprintf("%02x", b) 17 | } 18 | return strings.Join(hex, " ") 19 | } 20 | 21 | func (value OctetString) MarshalJSON() ([]byte, error) { 22 | return json.Marshal(value.String()) 23 | } 24 | 25 | type OctetStringSyntax struct{} 26 | 27 | func (syntax OctetStringSyntax) UnpackIndex(index []int) (Value, []int, error) { 28 | // TODO 29 | return nil, index, SyntaxIndexError{syntax, index} 30 | } 31 | 32 | func (syntax OctetStringSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 33 | snmpValue, err := varBind.Value() 34 | if err != nil { 35 | return nil, err 36 | } 37 | switch value := snmpValue.(type) { 38 | case []byte: 39 | return OctetString(value), nil 40 | default: 41 | return nil, SyntaxError{syntax, value} 42 | } 43 | } 44 | 45 | func init() { 46 | RegisterSyntax("OCTET STRING", OctetStringSyntax{}) 47 | } 48 | -------------------------------------------------------------------------------- /mibs/syntax_oid.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | ) 8 | 9 | type OID snmp.OID 10 | 11 | func (value OID) String() string { 12 | return fmt.Sprintf("%v", snmp.OID(value)) 13 | } 14 | 15 | func (value OID) MarshalJSON() ([]byte, error) { 16 | return json.Marshal(value.String()) 17 | } 18 | 19 | type ObjectIdentifierSyntax struct{} 20 | 21 | func (syntax ObjectIdentifierSyntax) UnpackIndex(index []int) (Value, []int, error) { 22 | // TODO 23 | return nil, index, SyntaxIndexError{syntax, index} 24 | } 25 | 26 | func (syntax ObjectIdentifierSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 27 | snmpValue, err := varBind.Value() 28 | if err != nil { 29 | return nil, err 30 | } 31 | switch value := snmpValue.(type) { 32 | case []int: 33 | return OID(value), nil 34 | default: 35 | return nil, SyntaxError{syntax, value} 36 | } 37 | } 38 | 39 | func init() { 40 | RegisterSyntax("OBJECT IDENTIFIER", ObjectIdentifierSyntax{}) 41 | } 42 | -------------------------------------------------------------------------------- /mibs/syntax_physaddr.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | "strings" 8 | ) 9 | 10 | type PhysAddress []byte 11 | 12 | func (physAddress PhysAddress) String() string { 13 | var parts = make([]string, len(physAddress)) 14 | 15 | for i, octet := range physAddress { 16 | parts[i] = fmt.Sprintf("%02x", octet) 17 | } 18 | 19 | return strings.Join(parts, ":") 20 | } 21 | 22 | func (value PhysAddress) MarshalJSON() ([]byte, error) { 23 | return json.Marshal(value.String()) 24 | } 25 | 26 | type PhysAddressSyntax struct{} 27 | 28 | func (syntax PhysAddressSyntax) UnpackIndex(index []int) (Value, []int, error) { 29 | // TODO 30 | return nil, index, SyntaxIndexError{syntax, index} 31 | } 32 | 33 | func (syntax PhysAddressSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 34 | snmpValue, err := varBind.Value() 35 | if err != nil { 36 | return nil, err 37 | } 38 | switch value := snmpValue.(type) { 39 | case []byte: 40 | return PhysAddress(value), nil 41 | default: 42 | return nil, SyntaxError{syntax, value} 43 | } 44 | } 45 | 46 | func init() { 47 | RegisterSyntax("SNMPv2-TC::PhysAddress", PhysAddressSyntax{}) 48 | } 49 | -------------------------------------------------------------------------------- /mibs/syntax_registry.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Used for config loading 9 | type SyntaxMap map[string]reflect.Type 10 | 11 | var syntaxMap = make(SyntaxMap) 12 | 13 | func RegisterSyntax(name string, syntax Syntax) { 14 | var syntaxType = reflect.TypeOf(syntax) 15 | 16 | syntaxMap[name] = syntaxType 17 | } 18 | 19 | // Returns pointer-valued interface suitable for unmarshalling 20 | func LookupSyntax(name string) (Syntax, error) { 21 | if syntaxType, ok := syntaxMap[name]; !ok { 22 | return nil, fmt.Errorf("Unknown Syntax %v", name) 23 | } else { 24 | var syntaxValue = reflect.New(syntaxType) 25 | 26 | return syntaxValue.Interface().(Syntax), nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mibs/syntax_test.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | var testDisplayStringSyntax Syntax = DisplayStringSyntax{} 4 | var testEnumSyntax Syntax = EnumSyntax{} 5 | var testGaugeSyntax Syntax = GaugeSyntax{} 6 | var testObjectIdentifierSyntax Syntax = ObjectIdentifierSyntax{} 7 | var testPhysAddressSyntax Syntax = PhysAddressSyntax{} 8 | var testTimeTicksSyntax Syntax = TimeTicksSyntax{} 9 | var testCounterSyntax Syntax = CounterSyntax{} 10 | var testMACAddressSyntax Syntax = MACAddressSyntax{} 11 | var testOctetStringSyntax Syntax = OctetStringSyntax{} 12 | var testUnsignedSyntax Syntax = UnsignedSyntax{} 13 | var testIPAddressSyntax Syntax = IPAddressSyntax{} 14 | -------------------------------------------------------------------------------- /mibs/syntax_timeticks.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/snmp" 7 | "time" 8 | ) 9 | 10 | type TimeTicks time.Duration 11 | 12 | func unpackTimeTicks(value int) TimeTicks { 13 | return TimeTicks(time.Duration(value) * 10 * time.Millisecond) 14 | } 15 | 16 | func (value TimeTicks) Seconds() float64 { 17 | return time.Duration(value).Seconds() 18 | } 19 | 20 | func (value TimeTicks) String() string { 21 | return fmt.Sprintf("%v", time.Duration(value)) 22 | } 23 | 24 | func (value TimeTicks) MarshalJSON() ([]byte, error) { 25 | return json.Marshal(value.Seconds()) 26 | } 27 | 28 | type TimeTicksSyntax struct{} 29 | 30 | func (syntax TimeTicksSyntax) UnpackIndex(index []int) (Value, []int, error) { 31 | if len(index) < 1 || index[0] < 0 { 32 | return nil, index, SyntaxIndexError{syntax, index} 33 | } 34 | 35 | return unpackTimeTicks(index[0]), index[1:], nil 36 | } 37 | 38 | func (syntax TimeTicksSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 39 | snmpValue, err := varBind.Value() 40 | if err != nil { 41 | return nil, err 42 | } 43 | switch value := snmpValue.(type) { 44 | case snmp.TimeTicks32: 45 | return unpackTimeTicks(int(value)), nil 46 | default: 47 | return nil, SyntaxError{syntax, value} 48 | } 49 | } 50 | 51 | func init() { 52 | RegisterSyntax("TimeTicks", TimeTicksSyntax{}) 53 | } 54 | -------------------------------------------------------------------------------- /mibs/syntax_unsigned.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/snmpbot/snmp" 6 | ) 7 | 8 | type Unsigned uint 9 | 10 | func (value Unsigned) String() string { 11 | return fmt.Sprintf("%v", uint(value)) 12 | } 13 | 14 | type UnsignedSyntax struct{} 15 | 16 | func (syntax UnsignedSyntax) UnpackIndex(index []int) (Value, []int, error) { 17 | if len(index) < 1 || index[0] < 0 { 18 | return nil, index, SyntaxIndexError{syntax, index} 19 | } 20 | 21 | return Unsigned(index[0]), index[1:], nil 22 | } 23 | 24 | func (syntax UnsignedSyntax) Unpack(varBind snmp.VarBind) (Value, error) { 25 | snmpValue, err := varBind.Value() 26 | if err != nil { 27 | return nil, err 28 | } 29 | switch value := snmpValue.(type) { 30 | case int: 31 | if value <= 0 { 32 | return nil, SyntaxError{syntax, value} 33 | } 34 | return Unsigned(value), nil 35 | case snmp.Gauge32: 36 | return Unsigned(value), nil 37 | default: 38 | return nil, SyntaxError{syntax, value} 39 | } 40 | } 41 | 42 | func init() { 43 | RegisterSyntax("Unsigned32", UnsignedSyntax{}) 44 | } 45 | -------------------------------------------------------------------------------- /mibs/table.go: -------------------------------------------------------------------------------- 1 | package mibs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/qmsk/snmpbot/snmp" 8 | ) 9 | 10 | type EntrySyntax []*Object 11 | type EntryValues []Value 12 | type EntryMap map[IDKey]Value 13 | 14 | type EntryErrors []error 15 | 16 | func (errs *EntryErrors) add(err error) { 17 | *errs = append(*errs, err) 18 | } 19 | 20 | func (errs EntryErrors) Error() string { 21 | var strs = make([]string, len(errs)) 22 | 23 | for i, err := range errs { 24 | strs[i] = err.Error() 25 | } 26 | 27 | return strings.Join(strs, "; ") 28 | } 29 | 30 | func (entrySyntax EntrySyntax) OIDs() []snmp.OID { 31 | var oids = make([]snmp.OID, len(entrySyntax)) 32 | 33 | for i, entry := range entrySyntax { 34 | oids[i] = entry.OID 35 | } 36 | 37 | return oids 38 | } 39 | 40 | func indexEquals(expected []int, index []int) bool { 41 | if len(expected) != len(index) { 42 | return false 43 | } 44 | 45 | for i, x := range expected { 46 | if index[i] != x { 47 | return false 48 | } 49 | } 50 | 51 | return true 52 | } 53 | 54 | // Returns nil entries for objects with error values 55 | func (entrySyntax EntrySyntax) Unpack(varBinds []snmp.VarBind) ([]int, EntryValues, error) { 56 | var entryValues = make(EntryValues, len(entrySyntax)) 57 | var entryIndex []int 58 | var entryErrors EntryErrors 59 | 60 | if len(varBinds) != len(entrySyntax) { 61 | return nil, nil, fmt.Errorf("Invalid VarBinds[%v] for entry syntax: %v", varBinds, entrySyntax) 62 | } 63 | 64 | for i, entryObject := range entrySyntax { 65 | var varBind = varBinds[i] 66 | 67 | if err := varBind.ErrorValue(); err != nil { 68 | // skip unsupported columns 69 | } else if index := entryObject.OID.Index(varBind.OID()); index == nil { 70 | entryErrors.add(fmt.Errorf("Invalid VarBind[%v] OID for %v: %v", varBind.OID(), entryObject, entryObject.OID)) 71 | } else if entryIndex != nil && !indexEquals(entryIndex, index) { 72 | entryErrors.add(fmt.Errorf("Mismatching VarBind[%v] OID for %v: index %v != expected %v", varBind.OID(), entryObject, index, entryIndex)) 73 | } else if value, err := entryObject.Unpack(varBind); err != nil { 74 | entryErrors.add(fmt.Errorf("Invalid VarBind[%v] Value for %v: %v", varBind.OID(), entryObject, err)) 75 | } else { 76 | entryIndex = index 77 | entryValues[i] = value 78 | } 79 | } 80 | 81 | if entryErrors == nil { 82 | // interface with type but nil value does not compare equal to nil 83 | return entryIndex, entryValues, nil 84 | } else { 85 | return entryIndex, entryValues, entryErrors 86 | } 87 | } 88 | 89 | func (entrySyntax EntrySyntax) Map(varBinds []snmp.VarBind) (EntryMap, error) { 90 | var entryMap = make(EntryMap) 91 | 92 | for i, entryObject := range entrySyntax { 93 | var varBind = varBinds[i] 94 | 95 | if err := varBind.ErrorValue(); err != nil { 96 | // XXX: skip unsupported columns? 97 | } 98 | 99 | if index := entryObject.OID.Index(varBind.OID()); index == nil { 100 | return nil, fmt.Errorf("Invalid VarBind[%v] OID for %v: %v", varBind.OID(), entryObject, entryObject.OID) 101 | } 102 | 103 | if value, err := entryObject.Unpack(varBind); err != nil { 104 | return nil, fmt.Errorf("Invalid VarBind[%v] Value for %v: %v", varBind.OID(), entryObject, err) 105 | } else { 106 | entryMap[entryObject.ID.Key()] = value 107 | } 108 | } 109 | 110 | return entryMap, nil 111 | } 112 | 113 | type Table struct { 114 | ID 115 | 116 | IndexSyntax IndexSyntax 117 | EntrySyntax EntrySyntax 118 | } 119 | 120 | func (table Table) EntryOIDs() []snmp.OID { 121 | return table.EntrySyntax.OIDs() 122 | } 123 | 124 | func (table Table) Unpack(varBinds []snmp.VarBind) (IndexValues, EntryValues, error) { 125 | index, entryValues, entryErr := table.EntrySyntax.Unpack(varBinds) 126 | 127 | if index == nil { 128 | return nil, entryValues, entryErr 129 | } 130 | 131 | indexValues, indexErr := table.IndexSyntax.UnpackIndex(index) 132 | 133 | if indexErr != nil { 134 | return indexValues, entryValues, indexErr 135 | } else if entryErr != nil { 136 | return indexValues, entryValues, entryErr 137 | } else { 138 | return indexValues, entryValues, nil 139 | } 140 | } 141 | 142 | func (table Table) Map(varBinds []snmp.VarBind) (IndexMap, EntryMap, error) { 143 | if len(varBinds) != len(table.EntrySyntax) { 144 | return nil, nil, fmt.Errorf("Incorrect count of colums for Table<%v>: %d", table, len(varBinds)) 145 | } 146 | 147 | // XXX: assuming all entry objects have the same index... 148 | var index = table.EntrySyntax[0].OID.Index(varBinds[0].OID()) 149 | 150 | if entryMap, err := table.EntrySyntax.Map(varBinds); err != nil { 151 | return nil, nil, err 152 | } else if indexMap, err := table.IndexSyntax.MapIndex(index); err != nil { 153 | return nil, nil, err 154 | } else { 155 | return indexMap, entryMap, nil 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /mibs/test/TEST2-MIB.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "TEST2-MIB", 3 | "OID": ".1.0.2", 4 | "Objects": [ 5 | { 6 | "Name": "test", 7 | "OID": ".1.0.2.1.1", 8 | "Syntax": "SNMPv2-TC::DisplayString" 9 | }, 10 | { 11 | "Name": "testID", 12 | "OID": ".1.0.2.1.2.1", 13 | "Syntax": "Integer32" 14 | }, 15 | { 16 | "Name": "testName", 17 | "OID": ".1.0.2.1.2.2", 18 | "Syntax": "SNMPv2-TC::DisplayString" 19 | }, 20 | { 21 | "Name": "testName2", 22 | "OID": ".1.0.2.1.2.3", 23 | "Syntax": "SNMPv2-TC::DisplayString" 24 | }, 25 | { 26 | "Name": "testEnum", 27 | "OID": ".1.0.2.1.3", 28 | "Syntax": "ENUM", 29 | "SyntaxOptions": [ 30 | { "Value": 1, "Name": "one"}, 31 | { "Value": 2, "Name": "two"} 32 | ] 33 | }, 34 | { 35 | "Name": "extObject", 36 | "OID": ".1.1.5.1", 37 | "Syntax": "Integer32" 38 | }, 39 | { 40 | "Name": "testUnknownSyntax", 41 | "OID": ".1.0.2.1.4", 42 | "Syntax": null 43 | } 44 | ], 45 | "Tables": [ 46 | { 47 | "Name": "testTable", 48 | "OID": ".1.0.2.1.2", 49 | "IndexObjects": [ "TEST2-MIB::testID" ], 50 | "EntryObjects": [ "TEST2-MIB::testName" ], 51 | "EntryName": "testEntry" 52 | }, 53 | { 54 | "Name": "testTable2", 55 | "OID": ".1.0.2.1.5", 56 | "EntryObjects": [ "TEST2-MIB::testName2" ], 57 | "EntryName": "testEntry2", 58 | "AugmentsEntry": "TEST2-MIB::testEntry" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | /opt/ 2 | mib-import.txt 3 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | mkdir mibs 4 | virtualenv -p python3 opt 5 | ./opt/bin/pip install -r requirements.txt 6 | ./opt/bin/python3 mib-import.py --output-path=mibs -- Q-BRIDGE-MIB 7 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | pysmi 2 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BurntSushi/toml" 6 | "github.com/qmsk/snmpbot/client" 7 | "strings" 8 | ) 9 | 10 | type ConfigKeysError struct { 11 | Source string 12 | Keys []toml.Key 13 | } 14 | 15 | func (err ConfigKeysError) String() string { 16 | var strs = make([]string, len(err.Keys)) 17 | 18 | for i, key := range err.Keys { 19 | strs[i] = key.String() 20 | } 21 | 22 | return strings.Join(strs, " ") 23 | } 24 | 25 | func (err ConfigKeysError) Error() string { 26 | return fmt.Sprintf("Unexpected keys in %s: %s", err.Source, err.String()) 27 | } 28 | 29 | type Config struct { 30 | ClientOptions client.Options 31 | Hosts map[string]HostConfig 32 | } 33 | 34 | func (config *Config) LoadTOML(path string) error { 35 | if tomlMeta, err := toml.DecodeFile(path, config); err != nil { 36 | return err 37 | } else if undecodedKeys := tomlMeta.Undecoded(); len(undecodedKeys) > 0 { 38 | return ConfigKeysError{path, undecodedKeys} 39 | } 40 | 41 | for hostID, hostConfig := range config.Hosts { 42 | config.Hosts[hostID] = hostConfig 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /server/config_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/qmsk/snmpbot/mibs" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var testMIB *mibs.MIB 12 | var testMIBs MIBs 13 | 14 | func init() { 15 | if file, err := os.Open("test/TEST-MIB.json"); err != nil { 16 | panic(err) 17 | } else if mib, err := mibs.LoadMIB(file); err != nil { 18 | panic(err) 19 | } else { 20 | testMIB = mib 21 | } 22 | 23 | testMIBs = MakeMIBs(testMIB) 24 | } 25 | 26 | func TestEngineMIBs(t *testing.T) { 27 | var engine = makeTestEngine(testConfig{clientMock: true}) 28 | 29 | assert.Equal(t, []string{"TEST-MIB"}, engine.MIBs().Keys(), "Engine.MIBs()") 30 | } 31 | 32 | func TestEngineObjects(t *testing.T) { 33 | var engine = makeTestEngine(testConfig{clientMock: true}) 34 | 35 | assert.ElementsMatch(t, []string{"TEST-MIB::test", "TEST-MIB::testID", "TEST-MIB::testName", "TEST-MIB::testEnum"}, engine.Objects().Strings(), "Engine.Objects()") 36 | } 37 | 38 | func TestEngineTables(t *testing.T) { 39 | var engine = makeTestEngine(testConfig{clientMock: true}) 40 | 41 | assert.ElementsMatch(t, []string{"TEST-MIB::testTable"}, engine.Tables().Strings(), "Engine.Tables()") 42 | } 43 | -------------------------------------------------------------------------------- /server/engine.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/client" 5 | "github.com/qmsk/snmpbot/mibs" 6 | ) 7 | 8 | type engineClient interface { 9 | String() string 10 | Probe(ids []mibs.ID) ([]bool, error) 11 | WalkObjects(objects []*mibs.Object, f func(*mibs.Object, mibs.IndexValues, mibs.Value, error) error) error 12 | WalkTable(table *mibs.Table, f func(mibs.IndexValues, mibs.EntryValues, error) error) error 13 | } 14 | 15 | type Engine interface { 16 | ClientOptions() client.Options 17 | client(config client.Config) (engineClient, error) 18 | 19 | MIBs() MIBs 20 | Objects() Objects 21 | Tables() Tables 22 | 23 | Hosts() Hosts 24 | AddHost(host *Host) bool 25 | SetHost(host *Host) 26 | DelHost(host *Host) bool 27 | 28 | QueryObjects(query ObjectQuery) <-chan ObjectResult 29 | QueryTables(query TableQuery) <-chan TableResult 30 | } 31 | 32 | func newEngine(clientEngine *client.Engine) *engine { 33 | return &engine{ 34 | clientEngine: clientEngine, 35 | mibs: AllMIBs(), 36 | hosts: makeEngineHosts(), 37 | } 38 | } 39 | 40 | type engine struct { 41 | clientEngine *client.Engine 42 | clientOptions client.Options 43 | 44 | mibs MIBs 45 | hosts engineHosts 46 | } 47 | 48 | func (engine *engine) loadConfig(config Config) error { 49 | engine.clientOptions = config.ClientOptions 50 | 51 | for hostName, hostConfig := range config.Hosts { 52 | go engine.loadHost(HostID(hostName), hostConfig) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (engine *engine) loadHost(id HostID, config HostConfig) { 59 | host, err := loadHost(engine, id, config) 60 | 61 | if err != nil { 62 | log.Warnf("Failed to load host %v: %v", id, err) 63 | 64 | host.err = err 65 | } else { 66 | log.Infof("Loaded host %v", id) 67 | } 68 | 69 | if !engine.hosts.Add(host) { 70 | log.Errorf("Duplicate host %v!", id) 71 | } 72 | } 73 | 74 | func (engine *engine) ClientOptions() client.Options { 75 | return engine.clientOptions 76 | } 77 | 78 | func (engine *engine) client(config client.Config) (engineClient, error) { 79 | if c, err := client.NewClient(engine.clientEngine, config); err != nil { 80 | return nil, err 81 | } else { 82 | return mibs.MakeClient(c), nil 83 | } 84 | } 85 | 86 | func (engine *engine) MIBs() MIBs { 87 | return engine.mibs 88 | } 89 | 90 | func (engine *engine) Objects() Objects { 91 | // TODO: limit by MIBs? 92 | return AllObjects() 93 | } 94 | 95 | func (engine *engine) Tables() Tables { 96 | // TODO: limit by MIBs? 97 | return AllTables() 98 | } 99 | 100 | func (engine *engine) Hosts() Hosts { 101 | return engine.hosts.Copy() 102 | } 103 | 104 | func (engine *engine) AddHost(host *Host) bool { 105 | return engine.hosts.Add(host) 106 | } 107 | 108 | func (engine *engine) SetHost(host *Host) { 109 | engine.hosts.Set(host) 110 | } 111 | 112 | func (engine *engine) DelHost(host *Host) bool { 113 | return engine.hosts.Del(host) 114 | } 115 | 116 | func (engine *engine) QueryObjects(query ObjectQuery) <-chan ObjectResult { 117 | log.Infof("Query objects %v @ %v", query.Objects, query.Hosts) 118 | 119 | var q = objectQuery{ 120 | ObjectQuery: query, 121 | resultChan: make(chan ObjectResult), 122 | } 123 | 124 | go q.query() 125 | 126 | return q.resultChan 127 | } 128 | 129 | func (engine *engine) QueryTables(query TableQuery) <-chan TableResult { 130 | log.Infof("Query tables %v @ %v", query.Tables, query.Hosts) 131 | 132 | var q = tableQuery{ 133 | TableQuery: query, 134 | resultChan: make(chan TableResult), 135 | } 136 | 137 | go q.query() 138 | 139 | return q.resultChan 140 | } 141 | -------------------------------------------------------------------------------- /server/engine_client_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/client" 5 | "github.com/qmsk/snmpbot/mibs" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type testEngineClient struct { 10 | config client.Config 11 | 12 | mock *mock.Mock 13 | } 14 | 15 | func (c *testEngineClient) String() string { 16 | return c.config.String() 17 | } 18 | 19 | func (c *testEngineClient) Probe(ids []mibs.ID) ([]bool, error) { 20 | if c.mock != nil { 21 | var args = c.mock.MethodCalled("Probe", ids) 22 | 23 | return args.Get(0).([]bool), args.Error(1) 24 | } else { 25 | return nil, nil 26 | } 27 | } 28 | 29 | func (c *testEngineClient) WalkObjects(objects []*mibs.Object, f func(*mibs.Object, mibs.IndexValues, mibs.Value, error) error) error { 30 | return nil // TODO 31 | } 32 | 33 | func (c *testEngineClient) WalkTable(table *mibs.Table, f func(mibs.IndexValues, mibs.EntryValues, error) error) error { 34 | return nil // TODO 35 | } 36 | -------------------------------------------------------------------------------- /server/engine_hosts.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func makeEngineHosts() engineHosts { 8 | return engineHosts{ 9 | Hosts: make(Hosts), 10 | } 11 | } 12 | 13 | type engineHosts struct { 14 | mutex sync.Mutex 15 | 16 | Hosts 17 | } 18 | 19 | // Returns false if host already exists 20 | func (hosts *engineHosts) Add(host *Host) bool { 21 | hosts.mutex.Lock() 22 | defer hosts.mutex.Unlock() 23 | 24 | if _, exists := hosts.Hosts[host.id]; !exists { 25 | hosts.Hosts[host.id] = host 26 | return true 27 | } else { 28 | return false 29 | } 30 | } 31 | 32 | func (hosts *engineHosts) Set(host *Host) { 33 | hosts.mutex.Lock() 34 | defer hosts.mutex.Unlock() 35 | 36 | hosts.Hosts[host.id] = host 37 | } 38 | 39 | // Returns false if host does not exist 40 | func (hosts *engineHosts) Del(host *Host) bool { 41 | hosts.mutex.Lock() 42 | defer hosts.mutex.Unlock() 43 | 44 | if _, exists := hosts.Hosts[host.id]; exists { 45 | delete(hosts.Hosts, host.id) 46 | return true 47 | } else { 48 | return false 49 | } 50 | } 51 | 52 | func (hosts *engineHosts) Copy() Hosts { 53 | var copy = make(Hosts) 54 | 55 | hosts.mutex.Lock() 56 | defer hosts.mutex.Unlock() 57 | 58 | for hostID, host := range hosts.Hosts { 59 | copy[hostID] = host 60 | } 61 | 62 | return copy 63 | } 64 | -------------------------------------------------------------------------------- /server/engine_hosts_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "fmt" 8 | ) 9 | 10 | func TestEngineAddHosts(t *testing.T) { 11 | var engine = makeTestEngine(testConfig{}) 12 | var host1 = newHost(HostID("test1")) 13 | var host2 = newHost(HostID("test2")) 14 | 15 | assert.Truef(t, engine.AddHost(host1), "engine.AddHost %v: to empty hosts", host1) 16 | assert.Truef(t, engine.AddHost(host2), "engine.AddHost %v: to empty hosts", host2) 17 | assert.Falsef(t, engine.AddHost(host1), "engine.AddHost %v: to pre-existing hosts", host1) 18 | 19 | assert.Equalf(t, MakeHosts(host1, host2), engine.Hosts(), "engine.Hosts") 20 | } 21 | 22 | func TestEngineSetHost(t *testing.T) { 23 | var engine = makeTestEngine(testConfig{}) 24 | var host1 = newHost(HostID("test")) 25 | var host2 = newHost(HostID("test")) 26 | 27 | host1.err = fmt.Errorf("test 1") 28 | host2.err = fmt.Errorf("test 2") 29 | 30 | engine.SetHost(host1) 31 | engine.SetHost(host2) 32 | 33 | assert.Equalf(t, MakeHosts(host2), engine.Hosts(), "engine.Hosts") 34 | } 35 | 36 | func TestEngineDelHost(t *testing.T) { 37 | var engine = makeTestEngine(testConfig{}) 38 | var host1 = newHost(HostID("test1")) 39 | var host2 = newHost(HostID("test2")) 40 | 41 | engine.SetHost(host1) 42 | engine.SetHost(host2) 43 | 44 | assert.Truef(t, engine.DelHost(host1), "engine.DelHost %v: existing host", host1) 45 | 46 | assert.Equalf(t, MakeHosts(host2), engine.Hosts(), "engine.Hosts") 47 | } 48 | 49 | func TestEngineDelHostMissing(t *testing.T) { 50 | var engine = makeTestEngine(testConfig{}) 51 | var host1 = newHost(HostID("test1")) 52 | var host2 = newHost(HostID("test2")) 53 | 54 | engine.SetHost(host1) 55 | assert.Falsef(t, engine.DelHost(host2), "engine.DelHost %v: existing host", host2) 56 | 57 | assert.Equalf(t, MakeHosts(host1), engine.Hosts(), "engine.Hosts") 58 | } 59 | -------------------------------------------------------------------------------- /server/engine_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/client" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type testConfig struct { 9 | hosts map[HostID]HostConfig 10 | mibs MIBs 11 | 12 | clientMock bool 13 | } 14 | 15 | type testEngine struct { 16 | hosts engineHosts 17 | mibs MIBs 18 | 19 | mock.Mock 20 | clientMock *mock.Mock 21 | } 22 | 23 | func makeTestEngine(config testConfig) *testEngine { 24 | var engine = testEngine{ 25 | hosts: makeEngineHosts(), 26 | } 27 | 28 | if config.mibs != nil { 29 | engine.mibs = config.mibs 30 | } else { 31 | engine.mibs = testMIBs 32 | } 33 | 34 | if !config.clientMock { 35 | engine.On("client", mock.AnythingOfType("client.Config")).Return(nil) 36 | } else { 37 | engine.clientMock = new(mock.Mock) 38 | } 39 | 40 | for id, config := range config.hosts { 41 | if host, err := loadHost(&engine, id, config); err != nil { 42 | panic(err) 43 | } else if !engine.hosts.Add(host) { 44 | panic("host already added") 45 | } 46 | } 47 | 48 | return &engine 49 | } 50 | 51 | func (e *testEngine) ClientOptions() client.Options { 52 | return client.Options{ 53 | Community: "public", 54 | } 55 | } 56 | 57 | func (e *testEngine) mockClient(snmp string, clientErr error) { 58 | if clientOptions, err := client.ParseConfig(e.ClientOptions(), snmp); err != nil { 59 | panic(err) 60 | } else { 61 | e.On("client", clientOptions).Return(clientErr) 62 | } 63 | } 64 | 65 | func (e *testEngine) client(config client.Config) (engineClient, error) { 66 | var args = e.Called(config) 67 | var client = testEngineClient{ 68 | config: config, 69 | 70 | mock: e.clientMock, 71 | } 72 | 73 | return &client, args.Error(0) 74 | } 75 | 76 | func (e *testEngine) MIBs() MIBs { 77 | return e.mibs 78 | } 79 | 80 | func (e *testEngine) Objects() Objects { 81 | return AllObjects() 82 | } 83 | 84 | func (e *testEngine) Tables() Tables { 85 | return AllTables() 86 | } 87 | 88 | func (e *testEngine) Hosts() Hosts { 89 | return e.hosts.Copy() 90 | } 91 | 92 | func (e *testEngine) AddHost(host *Host) bool { 93 | return e.hosts.Add(host) 94 | } 95 | 96 | func (e *testEngine) SetHost(host *Host) { 97 | e.hosts.Set(host) 98 | } 99 | 100 | func (e *testEngine) DelHost(host *Host) bool { 101 | return e.hosts.Del(host) 102 | } 103 | 104 | func (e *testEngine) QueryObjects(query ObjectQuery) <-chan ObjectResult { 105 | var c = make(chan ObjectResult) 106 | 107 | defer close(c) 108 | 109 | // TODO 110 | return c 111 | } 112 | 113 | func (e *testEngine) QueryTables(query TableQuery) <-chan TableResult { 114 | var c = make(chan TableResult) 115 | 116 | defer close(c) 117 | 118 | // TODO 119 | return c 120 | } 121 | -------------------------------------------------------------------------------- /server/host.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qmsk/go-logging" 6 | "github.com/qmsk/go-web" 7 | "github.com/qmsk/snmpbot/api" 8 | "github.com/qmsk/snmpbot/client" 9 | "github.com/qmsk/snmpbot/mibs" 10 | ) 11 | 12 | type HostConfig struct { 13 | SNMP string 14 | 15 | // optional metadata 16 | Location string 17 | 18 | // optional, defaults to global config 19 | ClientOptions *client.Options 20 | } 21 | 22 | func newHost(id HostID) *Host { 23 | host := Host{id: id} 24 | host.log = logging.WithPrefix(log, fmt.Sprintf("Host<%v>", id)) 25 | 26 | return &host 27 | } 28 | 29 | func loadHost(engine Engine, id HostID, config HostConfig) (*Host, error) { 30 | var host = newHost(id) 31 | 32 | if err := host.init(engine, config); err != nil { 33 | return host, err 34 | } else if err := host.probe(engine.MIBs()); err != nil { 35 | return host, err 36 | } else { 37 | return host, nil 38 | } 39 | } 40 | 41 | type Host struct { 42 | id HostID 43 | log logging.PrefixLogging 44 | config HostConfig 45 | client engineClient 46 | 47 | mibs MIBs 48 | err error 49 | online bool 50 | } 51 | 52 | func (host *Host) String() string { 53 | return fmt.Sprintf("%v", host.id) 54 | } 55 | func (host *Host) Config() HostConfig { 56 | return host.config 57 | } 58 | 59 | func (host *Host) init(engine Engine, config HostConfig) error { 60 | var clientOptions = engine.ClientOptions() 61 | 62 | if config.ClientOptions != nil { 63 | clientOptions = *config.ClientOptions 64 | } 65 | 66 | if config.SNMP == "" { 67 | config.SNMP = string(host.id) 68 | } 69 | 70 | host.config = config 71 | 72 | host.log.Infof("Config: %#v", host.config) 73 | 74 | if clientConfig, err := client.ParseConfig(clientOptions, config.SNMP); err != nil { 75 | return err 76 | } else if client, err := engine.client(clientConfig); err != nil { 77 | return fmt.Errorf("NewClient %v: %v", host, err) 78 | } else { 79 | host.log.Infof("Connected client: %v", client) 80 | 81 | host.client = client 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (host *Host) probe(probeMIBs MIBs) error { 88 | var ids = probeMIBs.ListIDs() 89 | var mibs = make(MIBs) 90 | 91 | host.log.Infof("Probing MIBs: %v", probeMIBs) 92 | 93 | if probed, err := host.client.Probe(ids); err != nil { 94 | return fmt.Errorf("Probe %v: %v", host, err) 95 | } else { 96 | for i, ok := range probed { 97 | if ok { 98 | mibs.Add(ids[i].MIB) 99 | } 100 | } 101 | } 102 | 103 | // TODO: probe system::sysLocation? 104 | host.mibs = mibs 105 | host.online = true 106 | 107 | return nil 108 | } 109 | 110 | func (host *Host) IsUp() bool { 111 | return host.online 112 | } 113 | 114 | func (host *Host) MIBs() MIBs { 115 | return host.mibs 116 | } 117 | 118 | func (host *Host) Objects() Objects { 119 | return host.mibs.Objects() 120 | } 121 | 122 | func (host *Host) Tables() Tables { 123 | return host.mibs.Tables() 124 | } 125 | 126 | func (host *Host) resolveObject(name string) (*mibs.Object, error) { 127 | return mibs.ResolveObject(name) 128 | } 129 | 130 | func (host *Host) resolveTable(name string) (*mibs.Table, error) { 131 | return mibs.ResolveTable(name) 132 | } 133 | 134 | type hostRoute struct { 135 | engine Engine 136 | host *Host 137 | loadConfig *HostConfig 138 | put api.HostPUT 139 | } 140 | 141 | func (route *hostRoute) Index(name string) (web.Resource, error) { 142 | if route.loadConfig == nil { 143 | // pre-configured host 144 | } else if err := route.host.init(route.engine, *route.loadConfig); err != nil { 145 | return nil, err 146 | } else if err := route.host.probe(route.engine.MIBs()); err != nil { 147 | return nil, err 148 | } 149 | 150 | switch name { 151 | case "": 152 | return hostView{route.engine, route.host}, nil 153 | case "objects": 154 | return hostObjectsRoute(*route), nil 155 | case "tables": 156 | return hostTablesRoute(*route), nil 157 | default: 158 | return nil, nil 159 | } 160 | } 161 | 162 | func (route *hostRoute) GetREST() (web.Resource, error) { 163 | return hostView{host: route.host}.makeAPIIndex(), nil 164 | } 165 | 166 | func (route *hostRoute) IntoREST() interface{} { 167 | return &route.put 168 | } 169 | 170 | func (route *hostRoute) makeHostConfig() HostConfig { 171 | var options = route.engine.ClientOptions() 172 | 173 | if route.put.Community != "" { 174 | options.Community = route.put.Community 175 | } 176 | 177 | return HostConfig{ 178 | SNMP: route.put.SNMP, 179 | Location: route.put.Location, 180 | ClientOptions: &options, 181 | } 182 | } 183 | 184 | func (route *hostRoute) PutREST() (web.Resource, error) { 185 | var hostConfig = route.makeHostConfig() 186 | 187 | if host, err := loadHost(route.engine, route.host.id, hostConfig); err != nil { 188 | return nil, err 189 | } else { 190 | route.engine.SetHost(host) // replace 191 | 192 | return hostView{host: host}.makeAPIIndex(), nil 193 | } 194 | } 195 | 196 | func (route *hostRoute) DeleteREST() (web.Resource, error) { 197 | if exists := route.engine.DelHost(route.host); !exists { 198 | return nil, web.Errorf(404, "Host not configured: %v", route.host.id) 199 | } 200 | 201 | return nil, nil 202 | } 203 | 204 | type hostView struct { 205 | engine Engine 206 | host *Host 207 | } 208 | 209 | func (view hostView) makeMIBs() []api.MIBIndex { 210 | var mibs []api.MIBIndex 211 | 212 | for _, mib := range view.host.MIBs() { 213 | mibs = append(mibs, mibView{mib}.makeAPIIndex()) 214 | } 215 | 216 | return mibs 217 | } 218 | 219 | func (view hostView) makeObjects() []api.ObjectIndex { 220 | var objects []api.ObjectIndex 221 | 222 | for _, object := range view.host.Objects() { 223 | objects = append(objects, objectView{object}.makeAPIIndex()) 224 | } 225 | 226 | return objects 227 | } 228 | 229 | func (view hostView) makeTables() []api.TableIndex { 230 | var tables []api.TableIndex 231 | 232 | for _, table := range view.host.Tables() { 233 | tables = append(tables, tableView{table}.makeAPIIndex()) 234 | } 235 | 236 | return tables 237 | } 238 | 239 | func (view hostView) makeAPISNMP() string { 240 | if view.host.client == nil { 241 | return "" 242 | } 243 | 244 | return view.host.client.String() 245 | } 246 | 247 | func (view hostView) makeAPIError() *api.Error { 248 | if view.host.err == nil { 249 | return nil 250 | } 251 | 252 | return &api.Error{view.host.err} 253 | } 254 | 255 | func (view hostView) makeAPIIndex() api.HostIndex { 256 | return api.HostIndex{ 257 | ID: string(view.host.id), 258 | SNMP: view.makeAPISNMP(), 259 | Location: view.host.config.Location, 260 | Online: view.host.online, 261 | Error: view.makeAPIError(), 262 | } 263 | } 264 | 265 | func (view hostView) makeAPI() api.Host { 266 | return api.Host{ 267 | HostIndex: view.makeAPIIndex(), 268 | MIBs: view.makeMIBs(), 269 | Objects: view.makeObjects(), 270 | Tables: view.makeTables(), 271 | } 272 | } 273 | 274 | func (view hostView) GetREST() (web.Resource, error) { 275 | return view.makeAPI(), nil 276 | } 277 | 278 | type hostObjectsRoute hostRoute 279 | 280 | func (route hostObjectsRoute) Index(name string) (web.Resource, error) { 281 | if name == "" { 282 | return &objectsHandler{ 283 | engine: route.engine, 284 | hosts: MakeHosts(route.host), 285 | objects: route.host.Objects(), 286 | tables: route.host.Tables(), 287 | }, nil 288 | } else if object, err := route.host.resolveObject(name); err != nil { 289 | return nil, web.Errorf(404, "%v", err) 290 | } else { 291 | return &objectHandler{ 292 | engine: route.engine, 293 | hosts: MakeHosts(route.host), 294 | object: object, 295 | }, nil 296 | } 297 | } 298 | 299 | type hostTablesRoute hostRoute 300 | 301 | func (route hostTablesRoute) Index(name string) (web.Resource, error) { 302 | if name == "" { 303 | return &tablesHandler{ 304 | engine: route.engine, 305 | hosts: MakeHosts(route.host), 306 | tables: route.host.Tables(), 307 | }, nil 308 | } else if table, err := route.host.resolveTable(name); err != nil { 309 | return nil, web.Errorf(404, "%v", err) 310 | } else { 311 | return &tableHandler{ 312 | engine: route.engine, 313 | hosts: MakeHosts(route.host), 314 | table: table, 315 | }, nil 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /server/host_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/qmsk/snmpbot/mibs" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLoadHost(t *testing.T) { 12 | var engine = makeTestEngine(testConfig{}) 13 | 14 | var host, err = loadHost(engine, HostID("test"), HostConfig{}) 15 | 16 | assert.NoError(t, err, "loadHost") 17 | assert.Equalf(t, "test", host.String(), "Host.String()") 18 | assert.Equalf(t, "public@test", host.client.String(), "Host.String()") 19 | } 20 | 21 | func TestLoadHostConfig(t *testing.T) { 22 | var engine = makeTestEngine(testConfig{}) 23 | 24 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 25 | SNMP: "public@localhost", 26 | }) 27 | 28 | assert.NoError(t, err, "loadHost") 29 | assert.Equalf(t, "test", host.String(), "Host.String()") 30 | assert.Equalf(t, "public@localhost", host.client.String(), "Host.String()") 31 | } 32 | 33 | func TestLoadHostConfigLocation(t *testing.T) { 34 | var engine = makeTestEngine(testConfig{}) 35 | 36 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 37 | SNMP: "localhost", 38 | Location: "testing", 39 | }) 40 | 41 | assert.NoError(t, err, "loadHost") 42 | assert.Equalf(t, "test", host.String(), "Host.String()") 43 | assert.Equalf(t, "public@localhost", host.client.String(), "Host.client.String()") 44 | assert.Equalf(t, "testing", host.Config().Location, "Host.Config.Location") 45 | } 46 | 47 | func TestLoadHostConfigClientOptions(t *testing.T) { 48 | var engine = makeTestEngine(testConfig{}) 49 | var options = engine.ClientOptions() 50 | 51 | options.Community = "private" 52 | 53 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 54 | SNMP: "localhost", 55 | ClientOptions: &options, 56 | }) 57 | 58 | assert.NoError(t, err, "loadHost") 59 | assert.Equalf(t, "test", host.String(), "Host.String()") 60 | assert.Equalf(t, "private@localhost", host.client.String(), "Host.String()") 61 | } 62 | 63 | func TestLoadHostConfigError(t *testing.T) { 64 | var engine = makeTestEngine(testConfig{}) 65 | 66 | var _, err = loadHost(engine, HostID("test"), HostConfig{ 67 | SNMP: "localhost:asdf", 68 | }) 69 | 70 | assert.EqualErrorf(t, err, `parse "udp+snmp://localhost:asdf": invalid port ":asdf" after host`, "loadHost ParseConfig") 71 | } 72 | 73 | func TestLoadHostClientError(t *testing.T) { 74 | var engine = makeTestEngine(testConfig{clientMock: true}) 75 | 76 | engine.mockClient("localhost", fmt.Errorf("Test error")) 77 | 78 | var _, err = loadHost(engine, HostID("test"), HostConfig{ 79 | SNMP: "localhost", 80 | }) 81 | 82 | assert.EqualErrorf(t, err, "NewClient test: Test error", "loadHost client") 83 | } 84 | 85 | func TestLoadHostClientProbeError(t *testing.T) { 86 | var engine = makeTestEngine(testConfig{clientMock: true}) 87 | 88 | engine.mockClient("localhost", nil) 89 | engine.clientMock.On("Probe", []mibs.ID{testMIB.ID}).Return([]bool{false}, fmt.Errorf("Test error")) 90 | 91 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 92 | SNMP: "localhost", 93 | }) 94 | 95 | assert.EqualError(t, err, "Probe test: Test error", "loadHost probe") 96 | assert.Equalf(t, "test", host.String(), "Host.String()") 97 | assert.False(t, host.IsUp(), "Host.IsUp") 98 | } 99 | 100 | func TestLoadHostClientProbeTrue(t *testing.T) { 101 | var engine = makeTestEngine(testConfig{clientMock: true}) 102 | 103 | engine.mockClient("localhost", nil) 104 | engine.clientMock.On("Probe", []mibs.ID{testMIB.ID}).Return([]bool{true}, nil) 105 | 106 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 107 | SNMP: "localhost", 108 | }) 109 | 110 | assert.NoError(t, err, "loadHost") 111 | assert.Equal(t, "test", host.String(), "Host.String()") 112 | assert.True(t, host.IsUp(), "Host.IsUp") 113 | assert.Equal(t, testMIBs, host.MIBs(), "Host.MIBs()") 114 | } 115 | 116 | func TestLoadHostClientProbeFalse(t *testing.T) { 117 | var engine = makeTestEngine(testConfig{clientMock: true}) 118 | 119 | engine.mockClient("localhost", nil) 120 | engine.clientMock.On("Probe", []mibs.ID{testMIB.ID}).Return([]bool{false}, nil) 121 | 122 | var host, err = loadHost(engine, HostID("test"), HostConfig{ 123 | SNMP: "localhost", 124 | }) 125 | 126 | assert.NoError(t, err, "loadHost") 127 | assert.Equal(t, "test", host.String(), "Host.String()") 128 | assert.True(t, host.IsUp(), "Host.IsUp") 129 | assert.Empty(t, host.MIBs(), "Host.MIBs()") 130 | } 131 | -------------------------------------------------------------------------------- /server/hosts.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-web" 5 | "github.com/qmsk/snmpbot/api" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | type HostID string 11 | 12 | func MakeHosts(args ...*Host) Hosts { 13 | var hosts = make(Hosts, len(args)) 14 | 15 | for _, host := range args { 16 | hosts[host.id] = host 17 | } 18 | 19 | return hosts 20 | } 21 | 22 | type Hosts map[HostID]*Host 23 | 24 | func (hosts Hosts) Keys() []HostID { 25 | var keys = make([]HostID, 0, len(hosts)) 26 | 27 | for key, _ := range hosts { 28 | keys = append(keys, key) 29 | } 30 | 31 | return keys 32 | } 33 | 34 | func (hosts Hosts) String() string { 35 | var ss = make([]string, 0, len(hosts)) 36 | 37 | for _, host := range hosts { 38 | ss = append(ss, host.String()) 39 | } 40 | 41 | return "{" + strings.Join(ss, ", ") + "}" 42 | } 43 | 44 | func (hosts Hosts) Filter(filters ...string) Hosts { 45 | var filtered = make(Hosts) 46 | 47 | for hostID, host := range hosts { 48 | var match = false 49 | var name = host.String() 50 | 51 | for _, filter := range filters { 52 | if matched, _ := path.Match(filter, name); matched { 53 | match = true 54 | } 55 | } 56 | 57 | if match { 58 | filtered[hostID] = host 59 | } 60 | } 61 | 62 | return filtered 63 | } 64 | 65 | type hostsRoute struct { 66 | engine Engine 67 | hosts Hosts 68 | hostQuery api.HostQuery 69 | } 70 | 71 | func (route *hostsRoute) QueryREST() interface{} { 72 | return &route.hostQuery 73 | } 74 | 75 | func (route *hostsRoute) makeHostConfig() HostConfig { 76 | var options = route.engine.ClientOptions() 77 | 78 | if route.hostQuery.Community != "" { 79 | options.Community = route.hostQuery.Community 80 | } 81 | 82 | return HostConfig{ 83 | SNMP: route.hostQuery.SNMP, 84 | ClientOptions: &options, 85 | } 86 | } 87 | 88 | func (route *hostsRoute) Index(name string) (web.Resource, error) { 89 | if name == "" { 90 | return &hostsView{engine: route.engine, hosts: route.hosts}, nil 91 | } else if host, ok := route.hosts[HostID(name)]; ok { 92 | return &hostRoute{engine: route.engine, host: host}, nil 93 | } else { 94 | var host = newHost(HostID(name)) 95 | var hostConfig = route.makeHostConfig() 96 | 97 | return &hostRoute{ 98 | engine: route.engine, 99 | host: host, 100 | loadConfig: &hostConfig, // apply at route lookup 101 | }, nil 102 | } 103 | } 104 | 105 | type hostsView struct { 106 | engine Engine 107 | hosts Hosts 108 | post api.HostPOST 109 | } 110 | 111 | func (view hostsView) makeAPIIndex() []api.HostIndex { 112 | var items = make([]api.HostIndex, 0, len(view.hosts)) 113 | 114 | for _, host := range view.hosts { 115 | items = append(items, hostView{host: host}.makeAPIIndex()) 116 | } 117 | 118 | return items 119 | } 120 | 121 | func (view *hostsView) GetREST() (web.Resource, error) { 122 | return view.makeAPIIndex(), nil 123 | } 124 | 125 | func (view *hostsView) IntoREST() interface{} { 126 | return &view.post 127 | } 128 | 129 | func (view *hostsView) makeHostConfig() HostConfig { 130 | var options = view.engine.ClientOptions() 131 | 132 | if view.post.Community != "" { 133 | options.Community = view.post.Community 134 | } 135 | 136 | return HostConfig{ 137 | SNMP: view.post.SNMP, 138 | Location: view.post.Location, 139 | ClientOptions: &options, 140 | } 141 | } 142 | 143 | func (view *hostsView) PostREST() (web.Resource, error) { 144 | if host, err := loadHost(view.engine, HostID(view.post.ID), view.makeHostConfig()); err != nil { 145 | return nil, err 146 | } else if ok := view.engine.AddHost(host); !ok { 147 | return nil, web.Errorf(409, "Host already configured: %v", host.id) 148 | } else { 149 | return hostView{host: host}.makeAPIIndex(), nil 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /server/logging.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-logging" 5 | ) 6 | 7 | var log logging.Logging 8 | 9 | func SetLogging(l logging.Logging) { 10 | log = l 11 | } 12 | -------------------------------------------------------------------------------- /server/mibs.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-web" 5 | "github.com/qmsk/snmpbot/api" 6 | "github.com/qmsk/snmpbot/mibs" 7 | ) 8 | 9 | type MIBs map[string]*mibs.MIB 10 | 11 | func AllMIBs() MIBs { 12 | var mibMap = make(MIBs) 13 | 14 | mibs.WalkMIBs(func(mib *mibs.MIB) { 15 | mibMap[mib.Name] = mib 16 | }) 17 | 18 | return mibMap 19 | } 20 | 21 | func MakeMIBs(args ...*mibs.MIB) MIBs { 22 | var mibs = make(MIBs, len(args)) 23 | 24 | for _, mib := range args { 25 | mibs[mib.Name] = mib 26 | } 27 | 28 | return mibs 29 | } 30 | 31 | func (mibs MIBs) Keys() []string { 32 | var keys = make([]string, 0, len(mibs)) 33 | 34 | for key, _ := range mibs { 35 | keys = append(keys, key) 36 | } 37 | 38 | return keys 39 | } 40 | 41 | func (mibMap MIBs) ListIDs() []mibs.ID { 42 | var list = make([]mibs.ID, 0, len(mibMap)) 43 | 44 | for _, mib := range mibMap { 45 | list = append(list, mib.ID) 46 | } 47 | 48 | return list 49 | } 50 | 51 | func (mibMap MIBs) Add(mib *mibs.MIB) { 52 | mibMap[mib.Name] = mib 53 | } 54 | 55 | func (mibMap MIBs) Objects() Objects { 56 | var objects = make(Objects) 57 | 58 | mibs.WalkObjects(func(object *mibs.Object) { 59 | if _, ok := mibMap[object.MIB.Name]; !ok { 60 | return 61 | } 62 | if object.NotAccessible { 63 | return 64 | } 65 | objects.add(object) 66 | }) 67 | 68 | return objects 69 | } 70 | 71 | func (mibMap MIBs) Tables() Tables { 72 | var tables = make(Tables) 73 | 74 | mibs.WalkTables(func(table *mibs.Table) { 75 | if _, ok := mibMap[table.MIB.Name]; !ok { 76 | return 77 | } 78 | tables.add(table) 79 | }) 80 | 81 | return tables 82 | } 83 | 84 | type mibsRoute struct { 85 | mibs MIBs 86 | } 87 | 88 | func (route mibsRoute) Index(name string) (web.Resource, error) { 89 | if name == "" { 90 | return mibsView{route.mibs}, nil 91 | } else if mib, ok := route.mibs[name]; !ok { 92 | return nil, web.Errorf(404, "MIB not found: %v", name) 93 | } else { 94 | return mibView{mib}, nil 95 | } 96 | } 97 | 98 | type mibView struct { 99 | mib *mibs.MIB 100 | } 101 | 102 | func (view mibView) makeAPIIndex() api.MIBIndex { 103 | return api.MIBIndex{ 104 | ID: view.mib.String(), 105 | } 106 | } 107 | 108 | func (view mibView) makeAPI() api.MIB { 109 | var mib = api.MIB{ 110 | MIBIndex: view.makeAPIIndex(), 111 | Objects: mibObjectsView{view.mib}.makeAPIIndex(), 112 | Tables: mibTablesView{view.mib}.makeAPIIndex(), 113 | } 114 | 115 | return mib 116 | } 117 | 118 | func (view mibView) GetREST() (web.Resource, error) { 119 | return view.makeAPI(), nil 120 | } 121 | 122 | type mibsView struct { 123 | mibs MIBs 124 | } 125 | 126 | func (view mibsView) makeAPIIndex() []api.MIBIndex { 127 | var index []api.MIBIndex 128 | 129 | for _, mib := range view.mibs { 130 | index = append(index, mibView{mib}.makeAPIIndex()) 131 | } 132 | 133 | return index 134 | } 135 | 136 | func (view mibsView) makeAPI() []api.MIB { 137 | var rets []api.MIB 138 | 139 | for _, mib := range view.mibs { 140 | rets = append(rets, mibView{mib}.makeAPI()) 141 | } 142 | 143 | return rets 144 | } 145 | 146 | func (view mibsView) GetREST() (web.Resource, error) { 147 | return view.makeAPI(), nil 148 | } 149 | -------------------------------------------------------------------------------- /server/mibs_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "github.com/qmsk/go-web/webtest" 8 | "github.com/qmsk/snmpbot/api" 9 | ) 10 | 11 | func TestGetMibsIndex(t *testing.T) { 12 | var engine = makeTestEngine(testConfig{ 13 | mibs: testMIBs, 14 | }) 15 | 16 | // 17 | var apiIndexes []api.MIBIndex 18 | var testMibIndexes = []api.MIBIndex{ 19 | api.MIBIndex{ 20 | ID: "TEST-MIB", 21 | }, 22 | } 23 | 24 | webtest.TestAPI(t, webtest.APITest{ 25 | Handler: WebAPI(engine), 26 | Request: webtest.APIRequest{ 27 | Method: "GET", 28 | Target: "/mibs/", 29 | }, 30 | Response: webtest.APIResponse{ 31 | StatusCode: 200, 32 | Object: &apiIndexes, 33 | }, 34 | }) 35 | 36 | assert.Equal(t, testMibIndexes, apiIndexes, "response index") 37 | } 38 | 39 | func TestGetMibIndex(t *testing.T) { 40 | var engine = makeTestEngine(testConfig{ 41 | mibs: testMIBs, 42 | }) 43 | 44 | // 45 | var apiIndex api.MIB 46 | var testMibIndex = api.MIB{ 47 | MIBIndex: api.MIBIndex{ 48 | ID: "TEST-MIB", 49 | }, 50 | Objects: []api.ObjectIndex{ 51 | api.ObjectIndex{ 52 | ID: "TEST-MIB::test", 53 | }, 54 | api.ObjectIndex{ 55 | ID: "TEST-MIB::testID", 56 | }, 57 | api.ObjectIndex{ 58 | ID: "TEST-MIB::testName", 59 | IndexKeys: []string{"TEST-MIB::testID"}, 60 | }, 61 | api.ObjectIndex{ 62 | ID: "TEST-MIB::testEnum", 63 | }, 64 | }, 65 | Tables: []api.TableIndex{ 66 | api.TableIndex{ 67 | ID: "TEST-MIB::testTable", 68 | IndexKeys: []string{"TEST-MIB::testID"}, 69 | ObjectKeys: []string{"TEST-MIB::testName"}, 70 | }, 71 | }, 72 | } 73 | 74 | webtest.TestAPI(t, webtest.APITest{ 75 | Handler: WebAPI(engine), 76 | Request: webtest.APIRequest{ 77 | Method: "GET", 78 | Target: "/mibs/TEST-MIB", 79 | }, 80 | Response: webtest.APIResponse{ 81 | StatusCode: 200, 82 | Object: &apiIndex, 83 | }, 84 | }) 85 | 86 | assert.Equal(t, testMibIndex.MIBIndex, apiIndex.MIBIndex, "response index") 87 | assert.ElementsMatch(t, testMibIndex.Objects, apiIndex.Objects, "response index objects") 88 | assert.ElementsMatch(t, testMibIndex.Tables, apiIndex.Tables, "response index tables") 89 | } 90 | 91 | func TestGetMibNotFound(t *testing.T) { 92 | var engine = makeTestEngine(testConfig{ 93 | mibs: testMIBs, 94 | }) 95 | 96 | // 97 | webtest.TestAPI(t, webtest.APITest{ 98 | Handler: WebAPI(engine), 99 | Request: webtest.APIRequest{ 100 | Method: "GET", 101 | Target: "/mibs/TEST-MIBX", 102 | }, 103 | Response: webtest.APIResponse{ 104 | StatusCode: 404, 105 | Text: "MIB not found: TEST-MIBX" + "\n", 106 | }, 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /server/object.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-web" 5 | "github.com/qmsk/snmpbot/api" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | type ObjectID string 12 | 13 | func AllObjects() Objects { 14 | var objects = make(Objects) 15 | 16 | mibs.WalkObjects(func(object *mibs.Object) { 17 | if object.NotAccessible { 18 | return 19 | } 20 | objects.add(object) 21 | }) 22 | 23 | return objects 24 | } 25 | 26 | func MakeObjects(args ...*mibs.Object) Objects { 27 | var objects = make(Objects, len(args)) 28 | 29 | for _, object := range args { 30 | objects.add(object) 31 | } 32 | 33 | return objects 34 | } 35 | 36 | type Objects map[ObjectID]*mibs.Object 37 | 38 | func (objects Objects) add(object *mibs.Object) { 39 | objects[ObjectID(object.Key())] = object 40 | } 41 | 42 | func (objects Objects) exists(object *mibs.Object) bool { 43 | if _, exists := objects[ObjectID(object.Key())]; exists { 44 | return true 45 | } else { 46 | return false 47 | } 48 | } 49 | 50 | func (objects Objects) Keys() []ObjectID { 51 | var keys = make([]ObjectID, 0, len(objects)) 52 | 53 | for key, _ := range objects { 54 | keys = append(keys, key) 55 | } 56 | 57 | return keys 58 | } 59 | 60 | func (objects Objects) Strings() []string { 61 | var strings = make([]string, 0, len(objects)) 62 | 63 | for _, object := range objects { 64 | strings = append(strings, object.String()) 65 | } 66 | 67 | return strings 68 | } 69 | 70 | func (objects Objects) String() string { 71 | var ss = make([]string, 0, len(objects)) 72 | 73 | for _, object := range objects { 74 | ss = append(ss, object.String()) 75 | } 76 | 77 | return "{" + strings.Join(ss, ", ") + "}" 78 | } 79 | 80 | func (objects Objects) List() []*mibs.Object { 81 | var list = make([]*mibs.Object, 0, len(objects)) 82 | 83 | for _, object := range objects { 84 | list = append(list, object) 85 | } 86 | 87 | return list 88 | } 89 | 90 | func (objects Objects) Filter(filters ...string) Objects { 91 | var filtered = make(Objects) 92 | 93 | for objectID, object := range objects { 94 | var match = false 95 | var name = object.String() 96 | 97 | for _, filter := range filters { 98 | if matched, _ := path.Match(filter, name); matched { 99 | match = true 100 | } 101 | } 102 | 103 | if match { 104 | filtered[objectID] = object 105 | } 106 | } 107 | 108 | return filtered 109 | } 110 | 111 | // Select objects belonging to tables 112 | func (objects Objects) FilterTables(tables Tables) Objects { 113 | var filtered = make(Objects) 114 | 115 | for _, table := range tables { 116 | for _, object := range table.EntrySyntax { 117 | if object.NotAccessible { 118 | continue 119 | } 120 | 121 | if !objects.exists(object) { 122 | continue 123 | } 124 | 125 | filtered.add(object) 126 | } 127 | } 128 | 129 | log.Debugf("Filter %d => %d objects by tables: %#v", len(objects), len(filtered), tables) 130 | 131 | return filtered 132 | } 133 | 134 | type objectsRoute struct { 135 | engine Engine 136 | } 137 | 138 | func (route objectsRoute) Index(name string) (web.Resource, error) { 139 | if name == "" { 140 | return &objectsHandler{ 141 | engine: route.engine, 142 | hosts: route.engine.Hosts(), 143 | objects: route.engine.Objects(), 144 | tables: route.engine.Tables(), 145 | }, nil 146 | } else if object, err := mibs.ResolveObject(name); err != nil { 147 | return nil, web.Errorf(404, "%v", err) 148 | } else { 149 | return &objectHandler{ 150 | engine: route.engine, 151 | hosts: route.engine.Hosts(), 152 | object: object, 153 | }, nil 154 | } 155 | } 156 | 157 | func (route objectsRoute) makeIndex() api.IndexObjects { 158 | return api.IndexObjects{ 159 | Objects: objectsView{objects: route.engine.Objects()}.makeAPIIndex(), 160 | } 161 | } 162 | 163 | func (route objectsRoute) GetREST() (web.Resource, error) { 164 | return route.makeIndex(), nil 165 | } 166 | 167 | type objectView struct { 168 | object *mibs.Object 169 | } 170 | 171 | func (view objectView) makeIndexKeys() []string { 172 | if view.object.IndexSyntax == nil { 173 | return nil 174 | } 175 | 176 | var keys = make([]string, len(view.object.IndexSyntax)) 177 | 178 | for i, indexObject := range view.object.IndexSyntax { 179 | keys[i] = indexObject.String() 180 | } 181 | 182 | return keys 183 | } 184 | 185 | func (view objectView) makeAPIIndex() api.ObjectIndex { 186 | var index = api.ObjectIndex{ 187 | ID: view.object.String(), 188 | IndexKeys: view.makeIndexKeys(), 189 | } 190 | 191 | return index 192 | } 193 | 194 | func (view objectView) makeObjectIndex(indexValues mibs.IndexValues) api.ObjectIndexMap { 195 | if indexValues == nil { 196 | return nil 197 | } 198 | var indexMap = make(api.ObjectIndexMap) 199 | 200 | for i, indexObject := range view.object.IndexSyntax { 201 | indexMap[indexObject.String()] = indexValues[i] 202 | } 203 | 204 | return indexMap 205 | } 206 | 207 | func (view objectView) instanceFromResult(result ObjectResult) api.ObjectInstance { 208 | var object = api.ObjectInstance{ 209 | HostID: string(result.Host.id), 210 | Value: result.Value, 211 | } 212 | 213 | // XXX: should always match...? 214 | if result.Object == view.object { 215 | object.Index = view.makeObjectIndex(result.IndexValues) 216 | } 217 | 218 | return object 219 | } 220 | 221 | func (view objectView) errorFromResult(result ObjectResult) api.ObjectError { 222 | var ret = api.ObjectError{ 223 | HostID: string(result.Host.id), 224 | Value: result.Value, 225 | } 226 | 227 | // XXX: should always match...? 228 | if result.Object == view.object { 229 | ret.Index = view.makeObjectIndex(result.IndexValues) 230 | } 231 | 232 | ret.Error = api.Error{result.Error} 233 | 234 | return ret 235 | } 236 | 237 | type objectsView struct { 238 | objects Objects 239 | } 240 | 241 | func (view objectsView) makeAPIIndex() []api.ObjectIndex { 242 | var objects []api.ObjectIndex 243 | 244 | for _, object := range view.objects { 245 | objects = append(objects, objectView{object}.makeAPIIndex()) 246 | } 247 | 248 | return objects 249 | } 250 | 251 | type objectHandler struct { 252 | engine Engine 253 | hosts Hosts 254 | object *mibs.Object 255 | params api.ObjectQuery 256 | } 257 | 258 | func (handler *objectHandler) query() api.Object { 259 | var object = api.Object{ 260 | ObjectIndex: objectView{handler.object}.makeAPIIndex(), 261 | Instances: []api.ObjectInstance{}, 262 | } 263 | 264 | for result := range handler.engine.QueryObjects(ObjectQuery{ 265 | Hosts: handler.hosts, 266 | Objects: MakeObjects(handler.object), 267 | }) { 268 | if result.Error != nil { 269 | object.Errors = append(object.Errors, objectView{result.Object}.errorFromResult(result)) 270 | } else { 271 | object.Instances = append(object.Instances, objectView{result.Object}.instanceFromResult(result)) 272 | } 273 | } 274 | 275 | return object 276 | } 277 | 278 | func (handler *objectHandler) QueryREST() interface{} { 279 | return &handler.params 280 | } 281 | 282 | func (handler *objectHandler) GetREST() (web.Resource, error) { 283 | log.Debugf("GET .../objects/%v %#v", handler.object, handler.params) 284 | 285 | if handler.params.Hosts != nil { 286 | handler.hosts = handler.hosts.Filter(handler.params.Hosts...) 287 | } 288 | 289 | return handler.query(), nil 290 | } 291 | 292 | type objectsHandler struct { 293 | engine Engine 294 | hosts Hosts 295 | objects Objects 296 | tables Tables 297 | params api.ObjectsQuery 298 | } 299 | 300 | func (handler *objectsHandler) query() ([]*api.Object, error) { 301 | var objectMap = make(map[ObjectID]*api.Object, len(handler.objects)) 302 | var objects = make([]*api.Object, 0, len(handler.objects)) 303 | var err error 304 | 305 | for objectID, o := range handler.objects { 306 | var object = api.Object{ 307 | ObjectIndex: objectView{o}.makeAPIIndex(), 308 | Instances: []api.ObjectInstance{}, 309 | } 310 | 311 | objectMap[objectID] = &object 312 | objects = append(objects, &object) 313 | } 314 | 315 | for result := range handler.engine.QueryObjects(ObjectQuery{ 316 | Hosts: handler.hosts, 317 | Objects: handler.objects, 318 | }) { 319 | var object = objectMap[ObjectID(result.Object.Key())] 320 | 321 | if result.Error != nil { 322 | object.Errors = append(object.Errors, objectView{result.Object}.errorFromResult(result)) 323 | } else { 324 | object.Instances = append(object.Instances, objectView{result.Object}.instanceFromResult(result)) 325 | } 326 | } 327 | 328 | return objects, err 329 | } 330 | 331 | func (handler *objectsHandler) QueryREST() interface{} { 332 | return &handler.params 333 | } 334 | 335 | func (handler *objectsHandler) GetREST() (web.Resource, error) { 336 | log.Debugf("GET .../objects/ %#v", handler.params) 337 | 338 | if handler.params.Hosts != nil { 339 | handler.hosts = handler.hosts.Filter(handler.params.Hosts...) 340 | } 341 | 342 | if handler.params.Tables != nil { 343 | handler.objects = handler.objects.FilterTables(handler.tables.Filter(handler.params.Tables...)) 344 | } 345 | 346 | if handler.params.Objects != nil { 347 | handler.objects = handler.objects.Filter(handler.params.Objects...) 348 | } 349 | 350 | return handler.query() 351 | } 352 | 353 | type mibObjectsView struct { 354 | mib *mibs.MIB 355 | } 356 | 357 | func (view mibObjectsView) makeAPIIndex() []api.ObjectIndex { 358 | var objects []api.ObjectIndex 359 | 360 | view.mib.Walk(func(id mibs.ID) { 361 | if object := view.mib.Object(id); object != nil { 362 | objects = append(objects, objectView{object}.makeAPIIndex()) 363 | } 364 | }) 365 | 366 | return objects 367 | } 368 | -------------------------------------------------------------------------------- /server/object_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "github.com/qmsk/go-web/webtest" 8 | "github.com/qmsk/snmpbot/api" 9 | ) 10 | 11 | func TestGetObjectsIndex(t *testing.T) { 12 | var engine = makeTestEngine(testConfig{ 13 | mibs: testMIBs, 14 | }) 15 | 16 | // 17 | var apiIndexObjects api.IndexObjects 18 | var testIndexObjects = api.IndexObjects{ 19 | Objects: []api.ObjectIndex{ 20 | api.ObjectIndex{ 21 | ID: "TEST-MIB::test", 22 | }, 23 | api.ObjectIndex{ 24 | ID: "TEST-MIB::testID", 25 | }, 26 | api.ObjectIndex{ 27 | ID: "TEST-MIB::testName", 28 | IndexKeys: []string{"TEST-MIB::testID"}, 29 | }, 30 | api.ObjectIndex{ 31 | ID: "TEST-MIB::testEnum", 32 | }, 33 | }, 34 | } 35 | 36 | webtest.TestAPI(t, webtest.APITest{ 37 | Handler: WebAPI(engine), 38 | Request: webtest.APIRequest{ 39 | Method: "GET", 40 | Target: "/objects", 41 | }, 42 | Response: webtest.APIResponse{ 43 | StatusCode: 200, 44 | Object: &apiIndexObjects, 45 | }, 46 | }) 47 | 48 | assert.ElementsMatch(t, testIndexObjects.Objects, apiIndexObjects.Objects, "response index") 49 | } 50 | -------------------------------------------------------------------------------- /server/options.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/qmsk/snmpbot/client" 7 | ) 8 | 9 | type Options struct { 10 | ConfigFile string 11 | } 12 | 13 | func (options *Options) InitFlags() { 14 | flag.StringVar(&options.ConfigFile, "config", "", "Load TOML config") 15 | } 16 | 17 | func (options Options) LoadConfig(clientOptions client.Options) (Config, error) { 18 | var config = Config{ 19 | ClientOptions: clientOptions, 20 | } 21 | 22 | if options.ConfigFile == "" { 23 | log.Debugf("Not loading any config file") 24 | 25 | } else if err := config.LoadTOML(options.ConfigFile); err != nil { 26 | return config, fmt.Errorf("Failed to load config from %v: %v", options.ConfigFile, err) 27 | } else { 28 | log.Infof("Load config from %v", options.ConfigFile) 29 | } 30 | 31 | return config, nil 32 | } 33 | 34 | func (options Options) Engine(clientEngine *client.Engine, config Config) (Engine, error) { 35 | var engine = newEngine(clientEngine) 36 | 37 | if err := engine.loadConfig(config); err != nil { 38 | return nil, err 39 | } 40 | 41 | return engine, nil 42 | } 43 | -------------------------------------------------------------------------------- /server/query.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/snmpbot/mibs" 5 | "sync" 6 | ) 7 | 8 | // Object may not be set if Error 9 | type ObjectResult struct { 10 | Host *Host 11 | Object *mibs.Object 12 | IndexValues mibs.IndexValues 13 | Value interface{} 14 | Error error 15 | } 16 | 17 | type TableResult struct { 18 | Host *Host 19 | Table *mibs.Table 20 | IndexValues mibs.IndexValues 21 | EntryValues mibs.EntryValues 22 | Error error 23 | } 24 | 25 | type ObjectQuery struct { 26 | Hosts Hosts 27 | Objects Objects 28 | } 29 | 30 | type objectQuery struct { 31 | ObjectQuery 32 | resultChan chan ObjectResult 33 | waitGroup sync.WaitGroup 34 | } 35 | 36 | func (q *objectQuery) fail(host *Host, err error) { 37 | for _, object := range q.Objects { 38 | q.resultChan <- ObjectResult{Host: host, Object: object, Error: err} 39 | } 40 | } 41 | 42 | func (q *objectQuery) queryHost(host *Host) error { 43 | if err := host.client.WalkObjects(q.Objects.List(), func(object *mibs.Object, indexValues mibs.IndexValues, value mibs.Value, err error) error { 44 | q.resultChan <- ObjectResult{ 45 | Host: host, 46 | Object: object, 47 | IndexValues: indexValues, 48 | Value: value, 49 | Error: err, 50 | } 51 | return nil 52 | }); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (q *objectQuery) query() { 60 | defer close(q.resultChan) 61 | 62 | for _, host := range q.Hosts { 63 | q.waitGroup.Add(1) 64 | go func(host *Host) { 65 | defer q.waitGroup.Done() 66 | 67 | if err := q.queryHost(host); err != nil { 68 | q.fail(host, err) 69 | } 70 | }(host) 71 | } 72 | 73 | q.waitGroup.Wait() 74 | } 75 | 76 | type TableQuery struct { 77 | Hosts Hosts 78 | Tables Tables 79 | } 80 | 81 | type tableQuery struct { 82 | TableQuery 83 | resultChan chan TableResult 84 | waitGroup sync.WaitGroup 85 | } 86 | 87 | func (q *tableQuery) fail(host *Host, table *mibs.Table, err error) { 88 | q.resultChan <- TableResult{Host: host, Table: table, Error: err} 89 | } 90 | 91 | func (q *tableQuery) queryHostTable(host *Host, table *mibs.Table) error { 92 | if err := host.client.WalkTable(table, func(indexValues mibs.IndexValues, entryValues mibs.EntryValues, err error) error { 93 | q.resultChan <- TableResult{ 94 | Host: host, 95 | Table: table, 96 | IndexValues: indexValues, 97 | EntryValues: entryValues, 98 | Error: err, 99 | } 100 | return nil 101 | }); err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (q *tableQuery) query() { 109 | defer close(q.resultChan) 110 | 111 | for _, host := range q.Hosts { 112 | for _, table := range q.Tables { 113 | q.waitGroup.Add(1) 114 | go func(host *Host, table *mibs.Table) { 115 | defer q.waitGroup.Done() 116 | 117 | if err := q.queryHostTable(host, table); err != nil { 118 | q.fail(host, table, err) 119 | } 120 | }(host, table) 121 | } 122 | } 123 | 124 | q.waitGroup.Wait() 125 | } 126 | -------------------------------------------------------------------------------- /server/table.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-web" 5 | "github.com/qmsk/snmpbot/api" 6 | "github.com/qmsk/snmpbot/mibs" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | func FilterTableObjects(table *mibs.Table, filters ...string) *mibs.Table { 12 | var filteredTable = mibs.Table{ 13 | ID: table.ID, 14 | IndexSyntax: table.IndexSyntax, 15 | } 16 | 17 | for _, entryObject := range table.EntrySyntax { 18 | var match = false 19 | var name = entryObject.String() 20 | 21 | for _, filter := range filters { 22 | if matched, _ := path.Match(filter, name); matched { 23 | match = true 24 | } 25 | } 26 | 27 | if match { 28 | filteredTable.EntrySyntax = append(filteredTable.EntrySyntax, entryObject) 29 | } 30 | } 31 | 32 | return &filteredTable 33 | } 34 | 35 | type TableID string 36 | 37 | func AllTables() Tables { 38 | var tables = make(Tables) 39 | 40 | mibs.WalkTables(func(table *mibs.Table) { 41 | tables.add(table) 42 | }) 43 | 44 | return tables 45 | } 46 | 47 | func MakeTables(args ...*mibs.Table) Tables { 48 | var tables = make(Tables) 49 | 50 | for _, arg := range args { 51 | tables.add(arg) 52 | } 53 | 54 | return tables 55 | } 56 | 57 | type Tables map[TableID]*mibs.Table 58 | 59 | func (tables Tables) add(table *mibs.Table) { 60 | tables[TableID(table.Key())] = table 61 | } 62 | 63 | func (tables Tables) Keys() []TableID { 64 | var keys = make([]TableID, 0, len(tables)) 65 | 66 | for key, _ := range tables { 67 | keys = append(keys, key) 68 | } 69 | 70 | return keys 71 | } 72 | 73 | func (tables Tables) Strings() []string { 74 | var strings = make([]string, 0, len(tables)) 75 | 76 | for _, table := range tables { 77 | strings = append(strings, table.String()) 78 | } 79 | 80 | return strings 81 | } 82 | 83 | func (tables Tables) String() string { 84 | var ss = make([]string, 0, len(tables)) 85 | 86 | for _, table := range tables { 87 | ss = append(ss, table.String()) 88 | } 89 | 90 | return "{" + strings.Join(ss, ", ") + "}" 91 | } 92 | 93 | func (tables Tables) IDs() []mibs.ID { 94 | var ids = make([]mibs.ID, len(tables)) 95 | 96 | for _, table := range tables { 97 | ids = append(ids, table.ID) 98 | } 99 | 100 | return ids 101 | } 102 | 103 | func (tables Tables) Filter(filters ...string) Tables { 104 | var filtered = make(Tables) 105 | 106 | for tableID, table := range tables { 107 | var match = false 108 | var name = table.String() 109 | 110 | for _, filter := range filters { 111 | if matched, _ := path.Match(filter, name); matched { 112 | match = true 113 | } 114 | } 115 | 116 | if match { 117 | filtered[tableID] = table 118 | } 119 | } 120 | 121 | log.Debugf("Filter %d => %d tables: %#v", len(tables), len(filtered), filters) 122 | 123 | return filtered 124 | } 125 | 126 | func (tables Tables) FilterObjects(filters ...string) Tables { 127 | var filtered = make(Tables) 128 | 129 | for tableID, table := range tables { 130 | table = FilterTableObjects(table, filters...) 131 | 132 | if len(table.EntrySyntax) == 0 { 133 | // no table objects matched, elide 134 | continue 135 | } 136 | 137 | filtered[tableID] = table 138 | } 139 | 140 | return filtered 141 | } 142 | 143 | type tablesRoute struct { 144 | engine Engine 145 | } 146 | 147 | func (route tablesRoute) Index(name string) (web.Resource, error) { 148 | if name == "" { 149 | return &tablesHandler{ 150 | engine: route.engine, 151 | hosts: route.engine.Hosts(), 152 | tables: route.engine.Tables(), 153 | }, nil 154 | } else if table, err := mibs.ResolveTable(name); err != nil { 155 | return nil, web.Errorf(404, "%v", err) 156 | } else { 157 | return &tableHandler{ 158 | engine: route.engine, 159 | hosts: route.engine.Hosts(), 160 | table: table, 161 | }, nil 162 | } 163 | } 164 | 165 | func (route tablesRoute) makeIndex() api.IndexTables { 166 | return api.IndexTables{ 167 | Tables: tablesView{tables: route.engine.Tables()}.makeAPIIndex(), 168 | } 169 | } 170 | 171 | func (route tablesRoute) GetREST() (web.Resource, error) { 172 | return route.makeIndex(), nil 173 | } 174 | 175 | type tableView struct { 176 | table *mibs.Table 177 | } 178 | 179 | func (view tableView) makeAPIIndex() api.TableIndex { 180 | var index = api.TableIndex{ 181 | ID: view.table.String(), 182 | IndexKeys: make([]string, len(view.table.IndexSyntax)), 183 | ObjectKeys: make([]string, len(view.table.EntrySyntax)), 184 | } 185 | 186 | for i, indexObject := range view.table.IndexSyntax { 187 | index.IndexKeys[i] = indexObject.String() 188 | } 189 | for i, entryObject := range view.table.EntrySyntax { 190 | index.ObjectKeys[i] = entryObject.String() 191 | } 192 | 193 | return index 194 | } 195 | 196 | func (view tableView) entryFromResult(result TableResult) api.TableEntry { 197 | var entry = api.TableEntry{ 198 | HostID: string(result.Host.id), 199 | Index: make(api.TableIndexMap), 200 | Objects: make(api.TableObjectsMap), 201 | } 202 | 203 | for i, indexObject := range view.table.IndexSyntax { 204 | entry.Index[indexObject.String()] = result.IndexValues[i] 205 | } 206 | for i, entryObject := range view.table.EntrySyntax { 207 | entry.Objects[entryObject.String()] = result.EntryValues[i] 208 | } 209 | 210 | if result.Error == nil { 211 | } else if entryErrors, ok := result.Error.(mibs.EntryErrors); ok { 212 | entry.Errors = make([]api.Error, len(entryErrors)) 213 | 214 | for i, err := range entryErrors { 215 | entry.Errors[i] = api.Error{err} 216 | } 217 | 218 | } else { 219 | entry.Errors = []api.Error{ 220 | api.Error{result.Error}, 221 | } 222 | } 223 | 224 | return entry 225 | } 226 | 227 | func (view tableView) errorFromResult(result TableResult) api.TableError { 228 | return api.TableError{ 229 | HostID: string(result.Host.id), 230 | Error: api.Error{result.Error}, 231 | } 232 | } 233 | 234 | type tablesView struct { 235 | tables Tables 236 | } 237 | 238 | func (view tablesView) makeAPIIndex() []api.TableIndex { 239 | var tables = make([]api.TableIndex, 0, len(view.tables)) 240 | 241 | for _, table := range view.tables { 242 | tables = append(tables, tableView{table}.makeAPIIndex()) 243 | } 244 | 245 | return tables 246 | } 247 | 248 | type mibTablesView struct { 249 | mib *mibs.MIB 250 | } 251 | 252 | func (view mibTablesView) makeAPIIndex() []api.TableIndex { 253 | var tables []api.TableIndex 254 | 255 | view.mib.Walk(func(id mibs.ID) { 256 | if table := view.mib.Table(id); table != nil { 257 | tables = append(tables, tableView{table}.makeAPIIndex()) 258 | } 259 | }) 260 | 261 | return tables 262 | } 263 | 264 | type tableHandler struct { 265 | engine Engine 266 | hosts Hosts 267 | table *mibs.Table 268 | params api.TableQuery 269 | } 270 | 271 | func (handler *tableHandler) query() api.Table { 272 | var table = api.Table{ 273 | TableIndex: tableView{handler.table}.makeAPIIndex(), 274 | } 275 | 276 | for result := range handler.engine.QueryTables(TableQuery{ 277 | Hosts: handler.hosts, 278 | Tables: MakeTables(handler.table), 279 | }) { 280 | if result.IndexValues == nil || result.EntryValues == nil { 281 | table.Errors = append(table.Errors, tableView{result.Table}.errorFromResult(result)) 282 | } else { 283 | table.Entries = append(table.Entries, tableView{result.Table}.entryFromResult(result)) 284 | } 285 | } 286 | 287 | return table 288 | } 289 | 290 | func (handler *tableHandler) QueryREST() interface{} { 291 | return &handler.params 292 | } 293 | 294 | func (handler *tableHandler) GetREST() (web.Resource, error) { 295 | if handler.params.Hosts != nil { 296 | handler.hosts = handler.hosts.Filter(handler.params.Hosts...) 297 | } 298 | if handler.params.Objects != nil { 299 | handler.table = FilterTableObjects(handler.table, handler.params.Objects...) 300 | } 301 | 302 | return handler.query(), nil 303 | } 304 | 305 | type tablesHandler struct { 306 | engine Engine 307 | hosts Hosts 308 | tables Tables 309 | params api.TablesQuery 310 | } 311 | 312 | func (handler *tablesHandler) query() []*api.Table { 313 | var tableMap = make(map[TableID]*api.Table, len(handler.tables)) 314 | var tables = make([]*api.Table, 0, len(handler.tables)) 315 | 316 | for tableID, t := range handler.tables { 317 | var table = &api.Table{ 318 | TableIndex: tableView{t}.makeAPIIndex(), 319 | Entries: []api.TableEntry{}, 320 | } 321 | 322 | tableMap[tableID] = table 323 | tables = append(tables, table) 324 | } 325 | 326 | for result := range handler.engine.QueryTables(TableQuery{ 327 | Hosts: handler.hosts, 328 | Tables: handler.tables, 329 | }) { 330 | var table = tableMap[TableID(result.Table.Key())] 331 | 332 | if result.IndexValues == nil || result.EntryValues == nil { 333 | table.Errors = append(table.Errors, tableView{result.Table}.errorFromResult(result)) 334 | } else { 335 | table.Entries = append(table.Entries, tableView{result.Table}.entryFromResult(result)) 336 | } 337 | } 338 | 339 | return tables 340 | } 341 | 342 | func (handler *tablesHandler) QueryREST() interface{} { 343 | return &handler.params 344 | } 345 | 346 | func (handler *tablesHandler) GetREST() (web.Resource, error) { 347 | if handler.params.Hosts != nil { 348 | handler.hosts = handler.hosts.Filter(handler.params.Hosts...) 349 | } 350 | if handler.params.Tables != nil { 351 | handler.tables = handler.tables.Filter(handler.params.Tables...) 352 | } 353 | if handler.params.Objects != nil { 354 | handler.tables = handler.tables.FilterObjects(handler.params.Objects...) 355 | } 356 | 357 | return handler.query(), nil 358 | } 359 | -------------------------------------------------------------------------------- /server/table_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "github.com/qmsk/go-web/webtest" 8 | "github.com/qmsk/snmpbot/api" 9 | ) 10 | 11 | func TestGetTablesIndex(t *testing.T) { 12 | var engine = makeTestEngine(testConfig{ 13 | mibs: testMIBs, 14 | }) 15 | 16 | // 17 | var apiIndexTables api.IndexTables 18 | var testIndexTables = api.IndexTables{ 19 | Tables: []api.TableIndex{ 20 | api.TableIndex{ 21 | ID: "TEST-MIB::testTable", 22 | IndexKeys: []string{"TEST-MIB::testID"}, 23 | ObjectKeys: []string{"TEST-MIB::testName"}, 24 | }, 25 | }, 26 | } 27 | 28 | webtest.TestAPI(t, webtest.APITest{ 29 | Handler: WebAPI(engine), 30 | Request: webtest.APIRequest{ 31 | Method: "GET", 32 | Target: "/tables", 33 | }, 34 | Response: webtest.APIResponse{ 35 | StatusCode: 200, 36 | Object: &apiIndexTables, 37 | }, 38 | }) 39 | 40 | assert.Equal(t, testIndexTables, apiIndexTables, "response index") 41 | } 42 | -------------------------------------------------------------------------------- /server/test/TEST-MIB.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "TEST-MIB", 3 | "OID": ".1.0.1", 4 | "Objects": [ 5 | { 6 | "Name": "test", 7 | "OID": ".1.0.1.1.1", 8 | "Syntax": "SNMPv2-TC::DisplayString" 9 | }, 10 | { 11 | "Name": "testID", 12 | "OID": ".1.0.1.1.2.1", 13 | "Syntax": "Integer32" 14 | }, 15 | { 16 | "Name": "testName", 17 | "OID": ".1.0.1.1.2.2", 18 | "Syntax": "SNMPv2-TC::DisplayString" 19 | }, 20 | { 21 | "Name": "testEnum", 22 | "OID": ".1.0.1.1.3", 23 | "Syntax": "ENUM", 24 | "SyntaxOptions": [ 25 | { "Value": 1, "Name": "one"}, 26 | { "Value": 2, "Name": "two"} 27 | ] 28 | } 29 | ], 30 | "Tables": [ 31 | { 32 | "Name": "testTable", 33 | "OID": ".1.0.1.1.2", 34 | "IndexObjects": [ "TEST-MIB::testID" ], 35 | "EntryObjects": [ "TEST-MIB::testName" ], 36 | "EntryName": "testEntry" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /server/web.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/qmsk/go-web" 5 | "github.com/qmsk/snmpbot/api" 6 | ) 7 | 8 | func WebAPI(engine Engine) web.API { 9 | return web.MakeAPI(indexRoute{engine}) 10 | } 11 | 12 | type indexRoute struct { 13 | engine Engine 14 | } 15 | 16 | func (route indexRoute) Index(name string) (web.Resource, error) { 17 | switch name { 18 | case "": 19 | return indexView{route.engine}, nil 20 | case "mibs": 21 | return mibsRoute{route.engine.MIBs()}, nil 22 | case "objects": 23 | return objectsRoute{route.engine}, nil 24 | case "tables": 25 | return tablesRoute{route.engine}, nil 26 | case "hosts": 27 | return &hostsRoute{engine: route.engine, hosts: route.engine.Hosts()}, nil 28 | default: 29 | return nil, nil 30 | } 31 | } 32 | 33 | type indexView struct { 34 | engine Engine 35 | } 36 | 37 | func (view indexView) makeAPIIndex() api.Index { 38 | return api.Index{ 39 | MIBs: mibsView{view.engine.MIBs()}.makeAPIIndex(), 40 | IndexObjects: objectsRoute{view.engine}.makeIndex(), 41 | IndexTables: tablesRoute{view.engine}.makeIndex(), 42 | Hosts: hostsView{engine: view.engine, hosts: view.engine.Hosts()}.makeAPIIndex(), 43 | } 44 | } 45 | 46 | func (view indexView) GetREST() (web.Resource, error) { 47 | return view.makeAPIIndex(), nil 48 | } 49 | -------------------------------------------------------------------------------- /server/web_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "github.com/qmsk/go-web/webtest" 8 | "github.com/qmsk/snmpbot/api" 9 | ) 10 | 11 | func TestEngineGetIndex(t *testing.T) { 12 | var engine = makeTestEngine(testConfig{ 13 | hosts: map[HostID]HostConfig{ 14 | HostID("test"): HostConfig{ 15 | SNMP: "public@localhost", 16 | Location: "test", 17 | }, 18 | }, 19 | }) 20 | 21 | // 22 | var apiIndex api.Index 23 | var testIndex = api.Index{ 24 | Hosts: []api.HostIndex{ 25 | api.HostIndex{ 26 | ID: "test", 27 | SNMP: "public@localhost", 28 | Online: true, 29 | Location: "test", 30 | }, 31 | }, 32 | MIBs: []api.MIBIndex{ 33 | api.MIBIndex{ 34 | ID: "TEST-MIB", 35 | }, 36 | }, 37 | IndexObjects: api.IndexObjects{ 38 | Objects: []api.ObjectIndex{ 39 | api.ObjectIndex{ 40 | ID: "TEST-MIB::test", 41 | }, 42 | api.ObjectIndex{ 43 | ID: "TEST-MIB::testID", 44 | }, 45 | api.ObjectIndex{ 46 | ID: "TEST-MIB::testName", 47 | IndexKeys: []string{"TEST-MIB::testID"}, 48 | }, 49 | api.ObjectIndex{ 50 | ID: "TEST-MIB::testEnum", 51 | }, 52 | }, 53 | }, 54 | IndexTables: api.IndexTables{ 55 | Tables: []api.TableIndex{ 56 | api.TableIndex{ 57 | ID: "TEST-MIB::testTable", 58 | IndexKeys: []string{"TEST-MIB::testID"}, 59 | ObjectKeys: []string{"TEST-MIB::testName"}, 60 | }, 61 | }, 62 | }, 63 | } 64 | 65 | webtest.TestAPI(t, webtest.APITest{ 66 | Handler: WebAPI(engine), 67 | Request: webtest.APIRequest{ 68 | Method: "GET", 69 | Target: "/", 70 | }, 71 | Response: webtest.APIResponse{ 72 | StatusCode: 200, 73 | Object: &apiIndex, 74 | }, 75 | }) 76 | 77 | assert.ElementsMatch(t, testIndex.Hosts, apiIndex.Hosts, "response index Hosts") 78 | assert.ElementsMatch(t, testIndex.MIBs, apiIndex.MIBs, "response index MIBs") 79 | assert.ElementsMatch(t, testIndex.Objects, apiIndex.Objects, "response index Objects") 80 | assert.ElementsMatch(t, testIndex.Tables, apiIndex.Tables, "response index Tables") 81 | } 82 | 83 | func TestNotFound(t *testing.T) { 84 | var engine = makeTestEngine(testConfig{ 85 | mibs: testMIBs, 86 | }) 87 | 88 | // 89 | webtest.TestAPI(t, webtest.APITest{ 90 | Handler: WebAPI(engine), 91 | Request: webtest.APIRequest{ 92 | Method: "GET", 93 | Target: "/testx", 94 | }, 95 | Response: webtest.APIResponse{ 96 | StatusCode: 404, 97 | }, 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /snmp/bulk_pdu.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Very similar to the PDU type, but the error fields are replaced by parameters 10 | type BulkPDU struct { 11 | RequestID int 12 | NonRepeaters int 13 | MaxRepetitions int 14 | VarBinds []VarBind 15 | } 16 | 17 | func (pdu *BulkPDU) unpack(raw asn1.RawValue) error { 18 | return unpack(raw, pdu) 19 | } 20 | 21 | func (pdu BulkPDU) GetRequestID() int { 22 | return pdu.RequestID 23 | } 24 | 25 | func (pdu BulkPDU) String() string { 26 | var scalars []string 27 | var entries []string 28 | 29 | for i, varBind := range pdu.VarBinds { 30 | if i < pdu.NonRepeaters { 31 | scalars = append(scalars, varBind.String()) 32 | } else { 33 | entries = append(entries, varBind.String()) 34 | } 35 | } 36 | 37 | return fmt.Sprintf("[%v] + %dx[%v]", strings.Join(scalars, ", "), pdu.MaxRepetitions, strings.Join(entries, ", ")) 38 | } 39 | 40 | func (pdu BulkPDU) GetError() PDUError { 41 | return PDUError{} 42 | } 43 | 44 | func (pdu BulkPDU) Pack(meta PDUMeta) (asn1.RawValue, error) { 45 | return packSequence(asn1.ClassContextSpecific, int(meta.PDUType), 46 | meta.RequestID, 47 | pdu.NonRepeaters, 48 | pdu.MaxRepetitions, 49 | pdu.VarBinds, 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /snmp/generic_pdu.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type GenericPDU struct { 10 | RequestID int 11 | ErrorStatus ErrorStatus 12 | ErrorIndex int 13 | VarBinds []VarBind 14 | } 15 | 16 | func (pdu *GenericPDU) unpack(raw asn1.RawValue) error { 17 | return unpack(raw, pdu) 18 | } 19 | 20 | func (pdu GenericPDU) GetRequestID() int { 21 | return pdu.RequestID 22 | } 23 | 24 | func (pdu GenericPDU) String() string { 25 | if pdu.ErrorStatus != 0 { 26 | return fmt.Sprintf("!%v", pdu.ErrorStatus) 27 | } 28 | 29 | var varBinds = make([]string, len(pdu.VarBinds)) 30 | 31 | for i, varBind := range pdu.VarBinds { 32 | varBinds[i] = varBind.String() 33 | } 34 | 35 | return strings.Join(varBinds, ", ") 36 | } 37 | 38 | func (pdu GenericPDU) GetVarBind(index int) VarBind { 39 | if index < len(pdu.VarBinds) { 40 | return pdu.VarBinds[index] 41 | } else { 42 | return VarBind{} 43 | } 44 | } 45 | 46 | func (pdu GenericPDU) GetError() PDUError { 47 | return PDUError{ 48 | ErrorStatus: pdu.ErrorStatus, 49 | VarBind: pdu.GetVarBind(pdu.ErrorIndex), 50 | } 51 | } 52 | 53 | func (pdu GenericPDU) Pack(meta PDUMeta) (asn1.RawValue, error) { 54 | return packSequence(asn1.ClassContextSpecific, int(meta.PDUType), 55 | meta.RequestID, 56 | pdu.ErrorStatus, 57 | pdu.ErrorIndex, 58 | pdu.VarBinds, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /snmp/marshal.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | ) 7 | 8 | // pack value with custom class/tag, returning a RawValue for further use in sequences/etc. 9 | // returns RawValue with FullBytes set. The .Bytes will always be nil. 10 | func pack(cls int, tag int, value interface{}) (asn1.RawValue, error) { 11 | var params string 12 | var raw = asn1.RawValue{ 13 | Class: cls, 14 | Tag: tag, 15 | } 16 | 17 | // asn.MarshalWithParams does not allow marshalling non-raw nil values 18 | if value == nil { 19 | raw.Bytes = []byte{} // explicit empty value for nil 20 | 21 | if bytes, err := asn1.Marshal(raw); err != nil { 22 | return raw, err 23 | } else { 24 | raw.FullBytes = bytes 25 | } 26 | } else { 27 | switch raw.Class { 28 | case asn1.ClassUniversal: 29 | 30 | case asn1.ClassContextSpecific: 31 | params = fmt.Sprintf("tag:%d", raw.Tag) 32 | case asn1.ClassApplication: 33 | params = fmt.Sprintf("application,tag:%d", raw.Tag) 34 | default: 35 | return raw, fmt.Errorf("unable to unpack raw value with class=%d", raw.Class) 36 | } 37 | 38 | if bytes, err := asn1.MarshalWithParams(value, params); err != nil { 39 | return raw, err 40 | } else { 41 | raw.FullBytes = bytes 42 | } 43 | } 44 | 45 | return raw, nil 46 | } 47 | 48 | func packSequence(cls int, tag int, values ...interface{}) (asn1.RawValue, error) { 49 | var raw = asn1.RawValue{Class: cls, Tag: tag, IsCompound: true} 50 | 51 | for _, value := range values { 52 | if value == nil { 53 | raw.Bytes = append(raw.Bytes, asn1.NullBytes...) 54 | } else if bytes, err := asn1.Marshal(value); err != nil { 55 | return raw, err 56 | } else { 57 | raw.Bytes = append(raw.Bytes, bytes...) 58 | } 59 | } 60 | 61 | return raw, nil 62 | } 63 | 64 | func marshalSequence(cls int, tag int, values ...interface{}) ([]byte, error) { 65 | if raw, err := packSequence(cls, tag, values...); err != nil { 66 | return nil, err 67 | } else { 68 | return asn1.Marshal(raw) 69 | } 70 | } 71 | 72 | func marshal(obj interface{}) ([]byte, error) { 73 | return asn1.Marshal(obj) 74 | } 75 | -------------------------------------------------------------------------------- /snmp/oid.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type OID []int 10 | 11 | // panic on ParseOID errors 12 | func MustParseOID(str string) OID { 13 | if oid, err := ParseOID(str); err != nil { 14 | panic(err) 15 | } else { 16 | return oid 17 | } 18 | } 19 | 20 | func ParseOID(str string) (OID, error) { 21 | if str == "" { 22 | return nil, nil 23 | } else if str == "." { 24 | return OID{}, nil 25 | } else if str[0] != '.' { 26 | return nil, fmt.Errorf("Invalid OID: does not start with .") 27 | } else { 28 | str = str[1:] 29 | } 30 | 31 | var parts = strings.Split(str, ".") 32 | var oid = make(OID, len(parts)) 33 | 34 | for i, part := range parts { 35 | if id, err := strconv.Atoi(part); err != nil { 36 | return nil, fmt.Errorf("Invalid OID part: %v", part) 37 | } else { 38 | oid[i] = id 39 | } 40 | } 41 | 42 | return oid, nil 43 | } 44 | 45 | func (oid OID) String() (str string) { 46 | if oid == nil { 47 | return "" 48 | } 49 | if len(oid) == 0 { 50 | return "." 51 | } 52 | 53 | for _, id := range oid { 54 | str += fmt.Sprintf(".%d", id) 55 | } 56 | return str 57 | } 58 | 59 | func (oid OID) Copy() OID { 60 | var copy OID 61 | 62 | return append(copy, oid...) 63 | } 64 | 65 | // Extend this OID with the given ids, returning the new, more-specific, OID. 66 | func (oid OID) Extend(ids ...int) OID { 67 | return append(oid.Copy(), ids...) 68 | } 69 | 70 | // Compare two OIDs for equality 71 | func (oid OID) Equals(other OID) bool { 72 | if len(oid) != len(other) { 73 | return false 74 | } 75 | for i := range oid { 76 | if oid[i] != other[i] { 77 | return false 78 | } 79 | } 80 | return true 81 | } 82 | 83 | // Test if the given OID is a more-specific of this OID, returning the extended part if so. 84 | // Returns {} if the OIDs are an exact match 85 | // Returns nil if the OIDs do not match 86 | func (oid OID) Index(other OID) []int { 87 | if len(other) < len(oid) { 88 | return nil 89 | } 90 | 91 | for i := range oid { 92 | if oid[i] != other[i] { 93 | return nil 94 | } 95 | } 96 | 97 | if len(other) == len(oid) { 98 | return OID{} 99 | } else { 100 | return other[len(oid):] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /snmp/oid_test.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var testParseOID = []struct { 9 | str string 10 | oid OID 11 | }{ 12 | {"", nil}, 13 | {".", OID{}}, 14 | {".1", OID{1}}, 15 | {".1.3", OID{1, 3}}, 16 | } 17 | 18 | func TestParseOID(t *testing.T) { 19 | for _, test := range testParseOID { 20 | if oid, err := ParseOID(test.str); err != nil { 21 | t.Errorf("parse OID %v: %v", test.str, err) 22 | } else { 23 | assert.Equal(t, test.oid, oid, "ParseOID(%#v)", test.str) 24 | } 25 | } 26 | } 27 | 28 | var testOIDString = []struct { 29 | oid OID 30 | str string 31 | }{ 32 | {nil, ""}, 33 | {OID{}, "."}, 34 | {OID{1}, ".1"}, 35 | {OID{1, 3}, ".1.3"}, 36 | } 37 | 38 | func TestOIDString(t *testing.T) { 39 | for _, test := range testOIDString { 40 | str := test.oid.String() 41 | 42 | assert.Equal(t, test.str, str, "OID(%#v).String()", test.oid) 43 | } 44 | } 45 | 46 | var testOIDIndex = []struct { 47 | oid OID 48 | oid2 OID 49 | index []int 50 | }{ 51 | { 52 | OID{1, 3, 6, 1, 6, 3, 1}, 53 | OID{1, 3, 6, 1, 6, 3, 2}, 54 | nil, 55 | }, 56 | { 57 | OID{1, 3, 6, 1, 6, 3, 1, 1, 5, 1}, 58 | OID{1, 3, 6, 1, 6, 3, 1, 1, 5, 1}, 59 | []int{}, 60 | }, 61 | { 62 | OID{1, 3, 6, 1, 6, 3, 1}, 63 | OID{1, 3, 6, 1, 6, 3, 1, 1, 5, 1}, 64 | []int{1, 5, 1}, 65 | }, 66 | } 67 | 68 | func TestOIDIndex(t *testing.T) { 69 | for _, test := range testOIDIndex { 70 | index := test.oid.Index(test.oid2) 71 | 72 | assert.Equal(t, test.index, index, "OID(%#v).Index(%#v)", test.oid, test.oid2) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /snmp/packet.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | ) 7 | 8 | type Packet struct { 9 | Version Version 10 | Community []byte 11 | RawPDU asn1.RawValue 12 | } 13 | 14 | func (packet *Packet) Unmarshal(buf []byte) error { 15 | if err := unmarshal(buf, packet); err != nil { 16 | return err 17 | } 18 | 19 | if packet.RawPDU.Class != asn1.ClassContextSpecific { 20 | return fmt.Errorf("unexpected PDU: ASN.1 class %d", packet.RawPDU.Class) 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (packet *Packet) PDUType() PDUType { 27 | // assuming packet.PDU.Class == asn1.ClassContextSpecific 28 | return PDUType(packet.RawPDU.Tag) 29 | } 30 | 31 | func (packet *Packet) UnpackPDU() (PDUMeta, PDU, error) { 32 | return UnpackPDU(packet.RawPDU) 33 | } 34 | 35 | func (packet *Packet) PackPDU(meta PDUMeta, pdu PDU) error { 36 | if rawPDU, err := pdu.Pack(meta); err != nil { 37 | return err 38 | } else { 39 | packet.RawPDU = rawPDU 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (packet *Packet) Marshal() ([]byte, error) { 46 | return marshal(*packet) 47 | } 48 | -------------------------------------------------------------------------------- /snmp/packet_test.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "encoding/hex" 6 | "github.com/stretchr/testify/assert" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func decodeTestPacket(str string) []byte { 12 | str = regexp.MustCompile(`\s+|--.+`).ReplaceAllString(str, "") 13 | 14 | if buf, err := hex.DecodeString(str); err != nil { 15 | panic(err) 16 | } else { 17 | return buf 18 | } 19 | } 20 | 21 | // prepare a VarBind for use with assert.Equal() in tests 22 | func testVarBind(oid OID, value interface{}) VarBind { 23 | varBind := MakeVarBind(oid, value) 24 | 25 | // ensure that varBind has both Bytes *and* FullBytes, for comparisons with unmarshal 26 | if _, err := asn1.Unmarshal(varBind.RawValue.FullBytes, &varBind.RawValue); err != nil { 27 | panic(err) 28 | } 29 | 30 | return varBind 31 | } 32 | 33 | type packetTest struct { 34 | bytes []byte 35 | packet Packet 36 | meta PDUMeta 37 | pdu PDU 38 | } 39 | 40 | func testPacketMarshal(t *testing.T, test packetTest) { 41 | if err := test.packet.PackPDU(test.meta, test.pdu); err != nil { 42 | t.Fatalf("pdu.pack: %v", err) 43 | } 44 | 45 | if bytes, err := test.packet.Marshal(); err != nil { 46 | t.Fatalf("packet.marshal: %v", err) 47 | } else { 48 | assert.Equal(t, test.bytes, bytes) 49 | } 50 | } 51 | 52 | func testPacketUnmarshal(t *testing.T, test packetTest) { 53 | var packet Packet 54 | var pdu PDU 55 | 56 | err := packet.Unmarshal(test.bytes) 57 | if err != nil { 58 | t.Errorf("packet.Unmarshal: %v", err) 59 | return 60 | } 61 | 62 | pduMeta, pdu, err := packet.UnpackPDU() 63 | if err != nil { 64 | t.Errorf("packet.UnpackPDU: %v", err) 65 | return 66 | } 67 | 68 | assert.Equal(t, test.packet.Version, packet.Version) 69 | assert.Equal(t, test.packet.Community, packet.Community) 70 | assert.Equal(t, test.meta, pduMeta) 71 | assert.Equal(t, test.pdu, pdu) 72 | } 73 | 74 | func testPacket(t *testing.T, test packetTest) { 75 | testPacketMarshal(t, test) 76 | testPacketUnmarshal(t, test) 77 | } 78 | 79 | func TestPacketGetRequest(t *testing.T) { 80 | testPacket(t, packetTest{ 81 | bytes: decodeTestPacket(` 82 | 30 21 -- SEQUENCE 83 | 02 01 01 -- INTEGER version 84 | 04 06 70 75 62 6c 69 63 -- OCTET STRING community 85 | a1 14 -- GetNextRequest-PDU 86 | 02 02 05 39 -- INTEGER request-id 87 | 02 01 00 -- INTEGER error-status 88 | 02 01 00 -- INTEGER error-index 89 | 30 08 -- SEQUENCE variable-bindings 90 | 30 06 -- SEQUENCE 91 | 06 02 2b 06 -- OID name 92 | 05 00 -- NULL value 93 | `), 94 | packet: Packet{ 95 | Version: SNMPv2c, 96 | Community: []byte("public"), 97 | }, 98 | meta: PDUMeta{GetNextRequestType, 1337}, 99 | pdu: GenericPDU{ 100 | RequestID: 1337, 101 | VarBinds: []VarBind{ 102 | testVarBind(OID{1, 3, 6}, nil), 103 | }, 104 | }, 105 | }) 106 | } 107 | 108 | func TestPacketGetResponse(t *testing.T) { 109 | testPacket(t, packetTest{ 110 | bytes: decodeTestPacket(` 111 | 30 38 02 01 01 04 06 70 75 62 6c 69 63 a2 2b 02 112 | 04 01 7a 6d f3 02 01 00 02 01 00 30 1d 30 1b 06 113 | 08 2b 06 01 02 01 01 05 00 04 0f 55 42 4e 54 20 114 | 45 64 67 65 53 77 69 74 63 68 115 | `), 116 | packet: Packet{ 117 | Version: SNMPv2c, 118 | Community: []byte("public"), 119 | }, 120 | meta: PDUMeta{GetResponseType, 24800755}, 121 | pdu: GenericPDU{ 122 | RequestID: 24800755, 123 | VarBinds: []VarBind{ 124 | testVarBind(OID{1, 3, 6, 1, 2, 1, 1, 5, 0}, []byte("UBNT EdgeSwitch")), 125 | }, 126 | }, 127 | }) 128 | } 129 | 130 | func TestPacketCounter32(t *testing.T) { 131 | testPacket(t, packetTest{ 132 | bytes: decodeTestPacket(` 133 | 30 30 02 01 01 04 06 70 75 62 6c 69 63 a2 23 02 134 | 04 29 9e 37 ef 02 01 00 02 01 00 30 15 30 13 06 135 | 0a 2b 06 01 02 01 02 02 01 0a 01 41 05 00 a8 dc 136 | 8b 3b 137 | `), 138 | packet: Packet{ 139 | Version: SNMPv2c, 140 | Community: []byte("public"), 141 | }, 142 | meta: PDUMeta{GetResponseType, 698234863}, 143 | pdu: GenericPDU{ 144 | RequestID: 698234863, 145 | VarBinds: []VarBind{ 146 | testVarBind(OID{1, 3, 6, 1, 2, 1, 2, 2, 1, 10, 1}, Counter32(2833025851)), 147 | }, 148 | }, 149 | }) 150 | } 151 | 152 | func TestPacketNoSuchInstance(t *testing.T) { 153 | testPacket(t, packetTest{ 154 | bytes: decodeTestPacket(` 155 | 30 29 02 01 01 04 06 70 75 62 6c 69 63 a2 1c 02 156 | 04 47 6b 38 88 02 01 00 02 01 00 30 0e 30 0c 06 157 | 08 2b 06 01 02 01 01 05 01 81 00 158 | `), 159 | packet: Packet{ 160 | Version: SNMPv2c, 161 | Community: []byte("public"), 162 | }, 163 | meta: PDUMeta{GetResponseType, 1198209160}, 164 | pdu: GenericPDU{ 165 | RequestID: 1198209160, 166 | VarBinds: []VarBind{ 167 | testVarBind(OID{1, 3, 6, 1, 2, 1, 1, 5, 1}, NoSuchInstanceValue), 168 | }, 169 | }, 170 | }) 171 | } 172 | 173 | func TestPacketGetBulk(t *testing.T) { 174 | testPacket(t, packetTest{ 175 | bytes: decodeTestPacket(` 176 | 30 39 02 01 01 04 06 70 75 62 6c 69 63 a5 2c 02 177 | 04 2c 6a 76 19 02 01 00 02 01 0a 30 1e 30 0d 06 178 | 09 2b 06 01 02 01 02 02 01 01 05 00 30 0d 06 09 179 | 2b 06 01 02 01 02 02 01 02 05 00 180 | `), 181 | packet: Packet{ 182 | Version: SNMPv2c, 183 | Community: []byte("public"), 184 | }, 185 | meta: PDUMeta{GetBulkRequestType, 745174553}, 186 | pdu: BulkPDU{ 187 | RequestID: 745174553, 188 | NonRepeaters: 0, 189 | MaxRepetitions: 10, 190 | VarBinds: []VarBind{ 191 | testVarBind(MustParseOID(".1.3.6.1.2.1.2.2.1.1"), nil), 192 | testVarBind(MustParseOID(".1.3.6.1.2.1.2.2.1.2"), nil), 193 | }, 194 | }, 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /snmp/pdu.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | ) 7 | 8 | type PDUMeta struct { 9 | PDUType PDUType 10 | RequestID int 11 | } 12 | 13 | type PDUError struct { 14 | ErrorStatus ErrorStatus 15 | VarBind VarBind 16 | } 17 | 18 | type PDU interface { 19 | GetRequestID() int 20 | 21 | GetError() PDUError 22 | 23 | Pack(PDUMeta) (asn1.RawValue, error) 24 | } 25 | 26 | func UnpackPDU(raw asn1.RawValue) (PDUMeta, PDU, error) { 27 | var pduType = PDUType(raw.Tag) 28 | 29 | if raw.Class != asn1.ClassContextSpecific { 30 | return PDUMeta{PDUType: pduType}, nil, fmt.Errorf("unexpected PDU: ASN.1 class=%d tag=%d", raw.Class, raw.Tag) 31 | } 32 | 33 | switch pduType { 34 | case GetRequestType, GetNextRequestType, GetResponseType, SetRequestType: 35 | var pdu GenericPDU 36 | 37 | err := pdu.unpack(raw) 38 | 39 | return PDUMeta{pduType, pdu.RequestID}, pdu, err 40 | 41 | case GetBulkRequestType: 42 | var pdu BulkPDU 43 | 44 | err := pdu.unpack(raw) 45 | 46 | return PDUMeta{pduType, pdu.RequestID}, pdu, err 47 | 48 | default: 49 | return PDUMeta{PDUType: pduType}, nil, fmt.Errorf("Unknown PDUType=%v", pduType) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /snmp/snmp.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | type Version int 11 | 12 | const ( 13 | SNMPv1 Version = 0 14 | SNMPv2c Version = 1 15 | ) 16 | 17 | type PDUType int // context-specific 18 | 19 | const ( 20 | GetRequestType PDUType = 0 21 | GetNextRequestType PDUType = 1 22 | GetResponseType PDUType = 2 23 | SetRequestType PDUType = 3 24 | TrapV1Type PDUType = 4 25 | GetBulkRequestType PDUType = 5 26 | InformRequestType PDUType = 6 27 | TrapV2Type PDUType = 7 28 | ReportType PDUType = 8 29 | ) 30 | 31 | func (pduType PDUType) String() string { 32 | switch pduType { 33 | case GetRequestType: 34 | return "GetRequest" 35 | case GetNextRequestType: 36 | return "GetNextRequest" 37 | case GetResponseType: 38 | return "GetResponse" 39 | case SetRequestType: 40 | return "SetRequest" 41 | case TrapV1Type: 42 | return "TrapV1" 43 | case GetBulkRequestType: 44 | return "GetBulkRequest" 45 | case InformRequestType: 46 | return "InformRequest" 47 | case TrapV2Type: 48 | return "TrapV2" 49 | case ReportType: 50 | return "Report" 51 | default: 52 | return fmt.Sprintf("PDUType(%d)", pduType) 53 | } 54 | } 55 | 56 | type GenericTrap int 57 | 58 | const ( 59 | TrapColdStart GenericTrap = 0 60 | TrapWarmStart GenericTrap = 1 61 | TrapLinkDown GenericTrap = 2 62 | TrapLinkUp GenericTrap = 3 63 | TrapAuthenticationFailure GenericTrap = 4 64 | TrapEgpNeighborLoss GenericTrap = 5 65 | TrapEnterpriseSpecific GenericTrap = 6 66 | ) 67 | 68 | type ErrorStatus int 69 | 70 | const ( 71 | Success ErrorStatus = 0 72 | TooBigError ErrorStatus = 1 73 | NoSuchNameError ErrorStatus = 2 74 | BadValueError ErrorStatus = 3 75 | ReadOnlyError ErrorStatus = 4 76 | GenericError ErrorStatus = 5 77 | ) 78 | 79 | func (err ErrorStatus) String() string { 80 | switch err { 81 | case Success: 82 | return "Success" 83 | case TooBigError: 84 | return "TooBig" 85 | case NoSuchNameError: 86 | return "NoSuchName" 87 | case BadValueError: 88 | return "BadValue" 89 | case ReadOnlyError: 90 | return "ReadOnly" 91 | case GenericError: 92 | return "GenericError" 93 | default: 94 | return fmt.Sprintf("ErrorStatus(%d)", err) 95 | } 96 | } 97 | 98 | func (err ErrorStatus) Error() string { 99 | return fmt.Sprintf("SNMP PDU Error: %s", err.String()) 100 | } 101 | 102 | type ErrorValue int // context-specific NULLs in VarBind.Value 103 | 104 | const ( 105 | NoSuchObjectValue ErrorValue = 0 106 | NoSuchInstanceValue ErrorValue = 1 107 | EndOfMibViewValue ErrorValue = 2 108 | ) 109 | 110 | func (err ErrorValue) String() string { 111 | switch err { 112 | case NoSuchObjectValue: 113 | return "NoSuchObject" 114 | case NoSuchInstanceValue: 115 | return "NoSuchInstance" 116 | case EndOfMibViewValue: 117 | return "EndOfMibView" 118 | default: 119 | return fmt.Sprintf("ErrorValue(%d)", err) 120 | } 121 | } 122 | 123 | func (err ErrorValue) Error() string { 124 | return fmt.Sprintf("SNMP VarBind Error: %s", err.String()) 125 | } 126 | 127 | type ApplicationValueType int // application-specific values in VarBind.Value 128 | 129 | const ( 130 | IPAddressType ApplicationValueType = 0 131 | Counter32Type ApplicationValueType = 1 132 | Gauge32Type ApplicationValueType = 2 133 | TimeTicks32Type ApplicationValueType = 3 134 | OpaqueType ApplicationValueType = 4 135 | Counter64Type ApplicationValueType = 6 136 | ) 137 | 138 | // SNMPv1 Trap-PDU 139 | type TrapPDU struct { 140 | Enterprise asn1.ObjectIdentifier 141 | AgentAddr net.IP // []byte 142 | GenericTrap GenericTrap 143 | SpecificTrap int 144 | TimeStamp time.Duration // int64 145 | VarBinds []VarBind 146 | } 147 | -------------------------------------------------------------------------------- /snmp/string_test.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | type varBindStringTest struct { 9 | oid OID 10 | value interface{} 11 | str string 12 | } 13 | 14 | func testVarBindString(t *testing.T, test varBindStringTest) { 15 | var varBind = MakeVarBind(test.oid, test.value) 16 | 17 | assert.Equal(t, test.str, varBind.String()) 18 | } 19 | 20 | func TestVarBindStringNull(t *testing.T) { 21 | testVarBindString(t, varBindStringTest{ 22 | oid: OID{1, 3, 6, 1, 2, 1, 1, 5, 0}, 23 | value: nil, 24 | str: "1.3.6.1.2.1.1.5.0", 25 | }) 26 | } 27 | 28 | func TestVarBindString(t *testing.T) { 29 | testVarBindString(t, varBindStringTest{ 30 | oid: OID{1, 3, 6, 1, 2, 1, 1, 5, 0}, 31 | value: []byte("qmsk-snmp test"), 32 | str: "1.3.6.1.2.1.1.5.0=[113 109 115 107 45 115 110 109 112 32 116 101 115 116]", 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /snmp/unmarshal.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | "github.com/geoffgarside/ber" 7 | ) 8 | 9 | func unpack(raw asn1.RawValue, value interface{}) error { 10 | var params string 11 | 12 | switch raw.Class { 13 | case asn1.ClassUniversal: 14 | 15 | case asn1.ClassContextSpecific: 16 | params = fmt.Sprintf("tag:%d", raw.Tag) 17 | case asn1.ClassApplication: 18 | params = fmt.Sprintf("application,tag:%d", raw.Tag) 19 | default: 20 | return fmt.Errorf("unable to unpack raw value with class=%d", raw.Class) 21 | } 22 | 23 | if _, err := ber.UnmarshalWithParams(raw.FullBytes, value, params); err != nil { 24 | return err 25 | } else { 26 | // ignore trailing bytes 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func unmarshal(data []byte, obj interface{}) error { 33 | if _, err := ber.Unmarshal(data, obj); err != nil { 34 | return err 35 | } else { 36 | // XXX: ignore trailing bytes 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /snmp/varbind.go: -------------------------------------------------------------------------------- 1 | package snmp 2 | 3 | import ( 4 | "encoding/asn1" 5 | "fmt" 6 | ) 7 | 8 | type IPAddress [4]uint8 9 | type Counter32 uint32 10 | type Gauge32 uint32 11 | type TimeTicks32 uint32 // duration of 1/100 s 12 | type Opaque []byte 13 | type Counter64 uint64 14 | 15 | // panics if unable to pack value 16 | func MakeVarBind(oid OID, value interface{}) VarBind { 17 | var varBind = VarBind{ 18 | Name: asn1.ObjectIdentifier(oid), 19 | } 20 | 21 | if err := varBind.Set(value); err != nil { 22 | panic(err) 23 | } 24 | 25 | return varBind 26 | } 27 | 28 | type VarBind struct { 29 | Name asn1.ObjectIdentifier 30 | RawValue asn1.RawValue 31 | } 32 | 33 | func (varBind VarBind) String() string { 34 | if len(varBind.Name) == 0 { 35 | return fmt.Sprintf(".") 36 | } else if value, err := varBind.Value(); err != nil { 37 | return fmt.Sprintf("!%v", varBind.Name) 38 | } else if value != nil { 39 | return fmt.Sprintf("%v=%v", varBind.Name, value) 40 | } else { 41 | return fmt.Sprintf("%v", varBind.Name) 42 | } 43 | } 44 | 45 | func (varBind VarBind) OID() OID { 46 | return OID(varBind.Name) 47 | } 48 | 49 | // Return ErrorValue if exists, otherwise nil 50 | func (varBind VarBind) ErrorValue() error { 51 | if varBind.RawValue.Class == asn1.ClassContextSpecific { 52 | return ErrorValue(varBind.RawValue.Tag) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (varBind VarBind) Value() (interface{}, error) { 59 | switch varBind.RawValue.Class { 60 | case asn1.ClassUniversal: 61 | if varBind.RawValue.Tag == asn1.TagNull { 62 | return nil, nil 63 | } else { 64 | var value interface{} 65 | 66 | return value, unpack(varBind.RawValue, &value) 67 | } 68 | 69 | case asn1.ClassApplication: 70 | switch ApplicationValueType(varBind.RawValue.Tag) { 71 | case IPAddressType: 72 | var value []byte 73 | 74 | if err := unpack(varBind.RawValue, &value); err != nil { 75 | return nil, err 76 | } else if len(value) != 4 { 77 | return nil, fmt.Errorf("Invalid IPAddress value: %#v", value) 78 | } else { 79 | var ipAddress IPAddress 80 | 81 | for i := 0; i < 4; i++ { 82 | ipAddress[i] = uint8(value[i]) 83 | } 84 | 85 | return ipAddress, nil 86 | } 87 | 88 | case Counter32Type: 89 | var value int 90 | 91 | if err := unpack(varBind.RawValue, &value); err != nil { 92 | return nil, err 93 | } else { 94 | return Counter32(value), nil 95 | } 96 | 97 | case Gauge32Type: 98 | var value int 99 | 100 | if err := unpack(varBind.RawValue, &value); err != nil { 101 | return nil, err 102 | } else { 103 | return Gauge32(value), nil 104 | } 105 | 106 | case TimeTicks32Type: 107 | var value int 108 | 109 | if err := unpack(varBind.RawValue, &value); err != nil { 110 | return nil, err 111 | } else { 112 | return TimeTicks32(value), nil 113 | } 114 | 115 | case OpaqueType: 116 | var value Opaque 117 | 118 | return value, unpack(varBind.RawValue, &value) 119 | 120 | case Counter64Type: 121 | var value int64 // XXX: no support for uint64? 122 | 123 | if err := unpack(varBind.RawValue, &value); err != nil { 124 | return nil, err 125 | } else { 126 | return Counter64(value), nil 127 | } 128 | 129 | default: 130 | return nil, fmt.Errorf("Unkown varbind value application tag=%d", varBind.RawValue.Tag) 131 | } 132 | 133 | case asn1.ClassContextSpecific: 134 | switch ErrorValue(varBind.RawValue.Tag) { 135 | case NoSuchObjectValue: 136 | return NoSuchObjectValue, nil 137 | 138 | case NoSuchInstanceValue: 139 | return NoSuchInstanceValue, nil 140 | 141 | case EndOfMibViewValue: 142 | return EndOfMibViewValue, nil 143 | 144 | default: 145 | return nil, fmt.Errorf("Unkown varbind value context-specific tag=%d", varBind.RawValue.Tag) 146 | } 147 | 148 | default: 149 | return nil, fmt.Errorf("Unkown varbind value class=%d", varBind.RawValue.Class) 150 | } 151 | } 152 | 153 | func (varBind *VarBind) Set(genericValue interface{}) error { 154 | switch value := genericValue.(type) { 155 | case nil: 156 | varBind.SetNull() 157 | case ErrorValue: 158 | return varBind.SetError(value) 159 | case IPAddress: 160 | return varBind.setApplication(IPAddressType, value) 161 | case Counter32: 162 | return varBind.setApplication(Counter32Type, int(value)) 163 | case Gauge32: 164 | return varBind.setApplication(Gauge32Type, int(value)) 165 | case TimeTicks32: 166 | return varBind.setApplication(TimeTicks32Type, int(value)) 167 | case Opaque: 168 | return varBind.setApplication(OpaqueType, value) 169 | default: 170 | if rawValue, err := pack(asn1.ClassUniversal, 0, value); err != nil { 171 | return err 172 | } else { 173 | varBind.RawValue = rawValue 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (varBind *VarBind) SetNull() { 181 | if rawValue, err := pack(asn1.ClassUniversal, asn1.TagNull, nil); err != nil { 182 | panic(err) 183 | } else { 184 | varBind.RawValue = rawValue 185 | } 186 | } 187 | 188 | func (varBind *VarBind) SetError(errorValue ErrorValue) error { 189 | if rawValue, err := pack(asn1.ClassContextSpecific, int(errorValue), nil); err != nil { 190 | return err 191 | } else { 192 | varBind.RawValue = rawValue 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func (varBind *VarBind) setApplication(tag ApplicationValueType, value interface{}) error { 199 | if rawValue, err := pack(asn1.ClassApplication, int(tag), value); err != nil { 200 | return err 201 | } else { 202 | varBind.RawValue = rawValue 203 | } 204 | 205 | return nil 206 | } 207 | --------------------------------------------------------------------------------