bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/snmp/walk.go (about)

     1  package snmp
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  
     7  	"bosun.org/snmp/asn1"
     8  	"bosun.org/snmp/mib"
     9  )
    10  
    11  // Walk is a wrapper for SNMP.Walk.
    12  func Walk(host, community string, oids ...string) (*Rows, error) {
    13  	s, err := New(host, community)
    14  	if err != nil {
    15  		return nil, err
    16  	}
    17  	return s.Walk(oids...)
    18  }
    19  
    20  // Rows is the result of a walk. Its cursor starts before the first
    21  // row of the result set. Use Next to advance through the rows:
    22  //
    23  //     rows, err := snmp.Walk(host, community, "ifName")
    24  //     ...
    25  //     for rows.Next() {
    26  //         var name []byte
    27  //         err = rows.Scan(&name)
    28  //         ...
    29  //     }
    30  //     err = rows.Err() // get any error encountered during iteration
    31  //     ...
    32  type Rows struct {
    33  	avail    []row
    34  	last     row
    35  	walkFn   walkFunc
    36  	headText []string
    37  	head     []asn1.ObjectIdentifier
    38  	err      error
    39  	request  requestFunc
    40  }
    41  
    42  // row represents individual row.
    43  type row struct {
    44  	instance []int
    45  	bindings []binding
    46  }
    47  
    48  // Walk executes a query against host authenticated by the community string,
    49  // retrieving the MIB sub-tree defined by the the given root oids.
    50  func (s *SNMP) Walk(oids ...string) (*Rows, error) {
    51  	rows := &Rows{
    52  		avail:    nil,
    53  		walkFn:   walkN,
    54  		headText: oids,
    55  		head:     lookup(oids...),
    56  		request:  s.do,
    57  	}
    58  	for _, oid := range rows.head {
    59  		rows.last.bindings = append(rows.last.bindings, binding{Name: oid})
    60  	}
    61  	return rows, nil
    62  }
    63  
    64  // Next prepares the next result row for reading with the Scan method.
    65  // It returns true on success, false if there is no next result row.
    66  // Every call to Scan, even the first one, must be preceded by a call
    67  // to Next.
    68  func (rows *Rows) Next() bool {
    69  	if len(rows.avail) > 0 {
    70  		return true
    71  	}
    72  
    73  	if rows.err != nil {
    74  		if rows.err == io.EOF {
    75  			rows.err = nil
    76  		}
    77  		return false
    78  	}
    79  
    80  	row, err := rows.walkFn(rows.last.bindings, rows.request)
    81  	if err != nil {
    82  		if err == io.EOF {
    83  			rows.err = err
    84  		} else {
    85  			rows.err = fmt.Errorf("snmp.Walk: %v", err)
    86  		}
    87  		return false
    88  	}
    89  	rows.avail = row
    90  
    91  	for i, r := range rows.avail {
    92  		eof := 0
    93  		for i, b := range r.bindings {
    94  			if !hasPrefix(b.Name, rows.head[i]) {
    95  				eof++
    96  			}
    97  		}
    98  		if eof > 0 {
    99  			if eof < len(r.bindings) {
   100  				rows.err = fmt.Errorf("invalid response: pre-mature end of a column")
   101  				return false
   102  			}
   103  			rows.avail = rows.avail[:i]
   104  			rows.err = io.EOF
   105  			break
   106  		}
   107  	}
   108  
   109  	return len(rows.avail) > 0
   110  }
   111  
   112  // Scan copies the columns in the current row into the values pointed at by v.
   113  // On success, the id return variable will hold the row id of the current row.
   114  // It is typically an integer or a string.
   115  func (rows *Rows) Scan(v ...interface{}) (id interface{}, err error) {
   116  	if len(v) != len(rows.last.bindings) {
   117  		panic("snmp.Scan: invalid argument count")
   118  	}
   119  
   120  	cur := rows.avail[0]
   121  	rows.avail = rows.avail[1:]
   122  
   123  	last := rows.last
   124  	rows.last = cur
   125  
   126  	for i, a := range last.bindings {
   127  		b := cur.bindings[i]
   128  		if !a.less(b) {
   129  			return nil, fmt.Errorf("invalid response: %v: unordered binding: req=%+v >= resp=%+v",
   130  				rows.headText[i], a.Name, b.Name)
   131  		}
   132  	}
   133  
   134  	for i, b := range cur.bindings {
   135  		if err := b.unmarshal(v[i]); err != nil {
   136  			return nil, err
   137  		}
   138  	}
   139  
   140  	var want []int
   141  	for i, b := range cur.bindings {
   142  		offset := len(rows.head[i])
   143  		// BUG: out of bounds access
   144  		have := b.Name[offset:]
   145  		if i == 0 {
   146  			want = have
   147  			continue
   148  		}
   149  		if len(have) != len(want) || !hasPrefix(have, want) {
   150  			return nil, fmt.Errorf("invalid response: inconsistent instances")
   151  		}
   152  	}
   153  	id = convertInstance(want)
   154  
   155  	return id, nil
   156  }
   157  
   158  // convertInstance optionally converts the object instance id from the
   159  // general []byte form to simplified form: either a simple int, or a
   160  // string.
   161  func convertInstance(x []int) interface{} {
   162  	switch {
   163  	case len(x) == 1:
   164  		return x[0]
   165  	default:
   166  		s, ok := toStringInt(x)
   167  		if !ok {
   168  			return x
   169  		}
   170  		return s
   171  	}
   172  }
   173  
   174  // Err returns the error, if any, that was encountered during iteration.
   175  func (rows *Rows) Err() error {
   176  	return rows.err
   177  }
   178  
   179  type requestFunc func(*request) (*response, error)
   180  
   181  // walkFunc is a function that can request one or more rows.
   182  type walkFunc func([]binding, requestFunc) ([]row, error)
   183  
   184  // walk1 requests one row.
   185  func walk1(have []binding, rf requestFunc) ([]row, error) {
   186  	req := &request{
   187  		Type:     "GetNext",
   188  		ID:       <-nextID,
   189  		Bindings: have,
   190  	}
   191  	resp, err := rf(req)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	if err := check(resp, req); err != nil {
   196  		return nil, err
   197  	}
   198  	r := row{bindings: resp.Bindings}
   199  	return []row{r}, nil
   200  }
   201  
   202  // walkN requests a range of rows.
   203  func walkN(have []binding, rf requestFunc) ([]row, error) {
   204  	req := &request{
   205  		Type:           "GetBulk",
   206  		ID:             <-nextID,
   207  		Bindings:       have,
   208  		NonRepeaters:   0,
   209  		MaxRepetitions: 15,
   210  	}
   211  	resp, err := rf(req)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	if err := check(resp, req); err != nil {
   216  		return nil, err
   217  	}
   218  	received := resp.Bindings
   219  	sent := req.Bindings
   220  	if len(received)%len(sent) != 0 {
   221  		return nil, fmt.Errorf("invalid response: truncated bindings list")
   222  	}
   223  	var list []row
   224  	for len(received) > 0 {
   225  		list = append(list, row{bindings: received[:len(sent)]})
   226  		received = received[len(sent):]
   227  	}
   228  	if len(list) > req.MaxRepetitions {
   229  		return nil, fmt.Errorf("invalid response: peer violated MaxRepetitions, received %d rows, expected at most %d",
   230  			len(list), req.MaxRepetitions)
   231  	}
   232  	return list, nil
   233  }
   234  
   235  // lookup maps oids in their symbolic format into numeric format.
   236  func lookup(oids ...string) []asn1.ObjectIdentifier {
   237  	list := make([]asn1.ObjectIdentifier, 0, len(oids))
   238  	for _, o := range oids {
   239  		oid, err := mib.Lookup(o)
   240  		if err != nil {
   241  			panic(err)
   242  		}
   243  		list = append(list, oid)
   244  	}
   245  	return list
   246  }