2017-10-04 04:24:14 +02:00
|
|
|
package collector
|
2017-11-29 05:58:39 +01:00
|
|
|
|
|
|
|
import (
|
2019-07-03 02:46:19 +02:00
|
|
|
"crypto/md5"
|
2018-03-21 02:28:10 +01:00
|
|
|
"crypto/tls"
|
2019-07-03 02:46:19 +02:00
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
2020-02-04 04:03:45 +01:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2017-11-29 05:58:39 +01:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2019-11-16 06:05:31 +01:00
|
|
|
"mikrotik-exporter/config"
|
|
|
|
|
2020-02-04 04:03:45 +01:00
|
|
|
"github.com/miekg/dns"
|
2017-11-29 05:58:39 +01:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2017-11-30 04:42:59 +01:00
|
|
|
log "github.com/sirupsen/logrus"
|
2018-03-21 02:28:10 +01:00
|
|
|
routeros "gopkg.in/routeros.v2"
|
2017-11-29 05:58:39 +01:00
|
|
|
)
|
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
const (
|
|
|
|
namespace = "mikrotik"
|
2019-12-26 19:17:27 +01:00
|
|
|
apiPort = "8728"
|
|
|
|
apiPortTLS = "8729"
|
2020-02-04 04:03:45 +01:00
|
|
|
dnsPort = 53
|
2018-03-21 02:28:10 +01:00
|
|
|
|
|
|
|
// DefaultTimeout defines the default timeout when connecting to a router
|
|
|
|
DefaultTimeout = 5 * time.Second
|
|
|
|
)
|
2017-11-29 05:58:39 +01:00
|
|
|
|
|
|
|
var (
|
|
|
|
scrapeDurationDesc = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "scrape", "collector_duration_seconds"),
|
|
|
|
"mikrotik_exporter: duration of a collector scrape",
|
|
|
|
[]string{"device"},
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
scrapeSuccessDesc = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "scrape", "collector_success"),
|
|
|
|
"mikrotik_exporter: whether a collector succeeded",
|
|
|
|
[]string{"device"},
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
type collector struct {
|
|
|
|
devices []config.Device
|
2018-04-11 15:21:38 +02:00
|
|
|
collectors []routerOSCollector
|
2018-03-21 02:28:10 +01:00
|
|
|
timeout time.Duration
|
|
|
|
enableTLS bool
|
|
|
|
insecureTLS bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithBGP enables BGP routing metrics
|
|
|
|
func WithBGP() Option {
|
|
|
|
return func(c *collector) {
|
2018-05-21 22:56:21 +02:00
|
|
|
c.collectors = append(c.collectors, newBGPCollector())
|
2018-03-21 02:28:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithRoutes enables routing table metrics
|
|
|
|
func WithRoutes() Option {
|
|
|
|
return func(c *collector) {
|
2018-04-11 15:21:38 +02:00
|
|
|
c.collectors = append(c.collectors, newRoutesCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithDHCP enables DHCP serrver metrics
|
|
|
|
func WithDHCP() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newDHCPCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-02 20:50:20 +02:00
|
|
|
// WithDHCPL enables DHCP server leases
|
|
|
|
func WithDHCPL() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newDHCPLCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-11 15:21:38 +02:00
|
|
|
// WithDHCPv6 enables DHCPv6 serrver metrics
|
|
|
|
func WithDHCPv6() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newDHCPv6Collector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-10 19:55:10 +01:00
|
|
|
// WithHealth enables board Health metrics
|
|
|
|
func WithHealth() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newhealthCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-25 03:12:19 +02:00
|
|
|
// WithPOE enables PoE metrics
|
|
|
|
func WithPOE() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newPOECollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-11 15:21:38 +02:00
|
|
|
// WithPools enables IP(v6) pool metrics
|
|
|
|
func WithPools() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newPoolCollector())
|
2018-03-21 02:28:10 +01:00
|
|
|
}
|
2017-11-29 05:58:39 +01:00
|
|
|
}
|
|
|
|
|
2018-05-07 20:31:19 +02:00
|
|
|
// WithOptics enables optical diagnstocs
|
|
|
|
func WithOptics() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newOpticsCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-26 19:20:24 +01:00
|
|
|
// WithW60G enables w60g metrics
|
|
|
|
func WithW60G() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, neww60gInterfaceCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-11 05:30:13 +02:00
|
|
|
// WithWlanSTA enables wlan STA metrics
|
|
|
|
func WithWlanSTA() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newWlanSTACollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithWlanIF enables wireless interface metrics
|
|
|
|
func WithWlanIF() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newWlanIFCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-21 04:11:03 +02:00
|
|
|
// WithMonitor enables ethernet monitor collector metrics
|
|
|
|
func Monitor() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newMonitorCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
// WithTimeout sets timeout for connecting to router
|
|
|
|
func WithTimeout(d time.Duration) Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.timeout = d
|
|
|
|
}
|
|
|
|
}
|
2017-11-29 05:58:39 +01:00
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
// WithTLS enables TLS
|
|
|
|
func WithTLS(insecure bool) Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.enableTLS = true
|
|
|
|
c.insecureTLS = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-25 03:11:05 +02:00
|
|
|
// WithIpsec enables ipsec metrics
|
|
|
|
func WithIpsec() Option {
|
|
|
|
return func(c *collector) {
|
|
|
|
c.collectors = append(c.collectors, newIpsecCollector())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
// Option applies options to collector
|
|
|
|
type Option func(*collector)
|
|
|
|
|
|
|
|
// NewCollector creates a collector instance
|
|
|
|
func NewCollector(cfg *config.Config, opts ...Option) (prometheus.Collector, error) {
|
2017-11-30 04:42:59 +01:00
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"numDevices": len(cfg.Devices),
|
|
|
|
}).Info("setting up collector for devices")
|
2017-11-29 05:58:39 +01:00
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
c := &collector{
|
|
|
|
devices: cfg.Devices,
|
|
|
|
timeout: DefaultTimeout,
|
2018-04-11 15:21:38 +02:00
|
|
|
collectors: []routerOSCollector{
|
|
|
|
newInterfaceCollector(),
|
|
|
|
newResourceCollector(),
|
2018-03-21 02:28:10 +01:00
|
|
|
},
|
|
|
|
}
|
2017-11-29 05:58:39 +01:00
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
for _, o := range opts {
|
|
|
|
o(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
2017-11-29 05:58:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Describe implements the prometheus.Collector interface.
|
2018-03-21 02:28:10 +01:00
|
|
|
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
|
2017-11-29 05:58:39 +01:00
|
|
|
ch <- scrapeDurationDesc
|
|
|
|
ch <- scrapeSuccessDesc
|
2018-03-21 02:28:10 +01:00
|
|
|
|
|
|
|
for _, co := range c.collectors {
|
|
|
|
co.describe(ch)
|
|
|
|
}
|
2017-11-29 05:58:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Collect implements the prometheus.Collector interface.
|
2018-03-21 02:28:10 +01:00
|
|
|
func (c *collector) Collect(ch chan<- prometheus.Metric) {
|
2017-11-29 05:58:39 +01:00
|
|
|
wg := sync.WaitGroup{}
|
2020-02-04 04:03:45 +01:00
|
|
|
|
|
|
|
var realDevices []config.Device
|
2018-03-21 02:28:10 +01:00
|
|
|
|
|
|
|
for _, dev := range c.devices {
|
2020-02-04 04:03:45 +01:00
|
|
|
if (config.SrvRecord{}) != dev.Srv {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"SRV": dev.Srv.Record,
|
|
|
|
}).Info("SRV configuration detected")
|
2020-02-10 19:55:10 +01:00
|
|
|
conf, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
|
2020-02-04 04:03:45 +01:00
|
|
|
dnsServer := net.JoinHostPort(conf.Servers[0], strconv.Itoa(dnsPort))
|
|
|
|
if (config.DnsServer{}) != dev.Srv.Dns {
|
|
|
|
dnsServer = net.JoinHostPort(dev.Srv.Dns.Address, strconv.Itoa(dev.Srv.Dns.Port))
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"DnsServer": dnsServer,
|
|
|
|
}).Info("Custom DNS config detected")
|
|
|
|
}
|
|
|
|
dnsMsg := new(dns.Msg)
|
|
|
|
dnsCli := new(dns.Client)
|
|
|
|
|
|
|
|
dnsMsg.RecursionDesired = true
|
|
|
|
dnsMsg.SetQuestion(dns.Fqdn(dev.Srv.Record), dns.TypeSRV)
|
|
|
|
r, _, err := dnsCli.Exchange(dnsMsg, dnsServer)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, k := range r.Answer {
|
|
|
|
if s, ok := k.(*dns.SRV); ok {
|
|
|
|
d := config.Device{}
|
|
|
|
d.Name = strings.TrimRight(s.Target, ".")
|
|
|
|
d.Address = strings.TrimRight(s.Target, ".")
|
|
|
|
d.User = dev.User
|
|
|
|
d.Password = dev.Password
|
|
|
|
c.getIdentity(&d)
|
|
|
|
realDevices = append(realDevices, d)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
realDevices = append(realDevices, dev)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
wg.Add(len(realDevices))
|
|
|
|
|
|
|
|
for _, dev := range realDevices {
|
2018-03-21 02:28:10 +01:00
|
|
|
go func(d config.Device) {
|
|
|
|
c.collectForDevice(d, ch)
|
2017-11-29 05:58:39 +01:00
|
|
|
wg.Done()
|
2018-03-21 02:28:10 +01:00
|
|
|
}(dev)
|
2017-11-29 05:58:39 +01:00
|
|
|
}
|
2018-03-21 02:28:10 +01:00
|
|
|
|
2017-11-29 05:58:39 +01:00
|
|
|
wg.Wait()
|
|
|
|
}
|
|
|
|
|
2020-02-04 04:03:45 +01:00
|
|
|
func (c *collector) getIdentity(d *config.Device) error {
|
|
|
|
cl, err := c.connect(d)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"device": d.Name,
|
|
|
|
"error": err,
|
|
|
|
}).Error("error dialing device")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer cl.Close()
|
|
|
|
reply, err := cl.Run("/system/identity/print")
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"device": d.Name,
|
|
|
|
"error": err,
|
|
|
|
}).Error("error fetching ethernet interfaces")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, id := range reply.Re {
|
|
|
|
d.Name = id.Map["name"]
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-21 02:28:10 +01:00
|
|
|
func (c *collector) collectForDevice(d config.Device, ch chan<- prometheus.Metric) {
|
2017-11-29 05:58:39 +01:00
|
|
|
begin := time.Now()
|
2018-03-21 02:28:10 +01:00
|
|
|
|
|
|
|
err := c.connectAndCollect(&d, ch)
|
|
|
|
|
2017-11-29 05:58:39 +01:00
|
|
|
duration := time.Since(begin)
|
|
|
|
var success float64
|
|
|
|
if err != nil {
|
2018-03-21 02:28:10 +01:00
|
|
|
log.Errorf("ERROR: %s collector failed after %fs: %s", d.Name, duration.Seconds(), err)
|
2017-11-29 05:58:39 +01:00
|
|
|
success = 0
|
|
|
|
} else {
|
2018-03-21 02:28:10 +01:00
|
|
|
log.Debugf("OK: %s collector succeeded after %fs.", d.Name, duration.Seconds())
|
2017-11-29 05:58:39 +01:00
|
|
|
success = 1
|
|
|
|
}
|
2018-03-21 02:28:10 +01:00
|
|
|
|
|
|
|
ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), d.Name)
|
|
|
|
ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, d.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *collector) connectAndCollect(d *config.Device, ch chan<- prometheus.Metric) error {
|
|
|
|
cl, err := c.connect(d)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"device": d.Name,
|
|
|
|
"error": err,
|
|
|
|
}).Error("error dialing device")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer cl.Close()
|
|
|
|
|
|
|
|
for _, co := range c.collectors {
|
2018-04-11 15:21:38 +02:00
|
|
|
ctx := &collectorContext{ch, d, cl}
|
|
|
|
err = co.collect(ctx)
|
2018-03-21 02:28:10 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *collector) connect(d *config.Device) (*routeros.Client, error) {
|
2019-07-03 02:46:19 +02:00
|
|
|
var conn net.Conn
|
|
|
|
var err error
|
|
|
|
|
|
|
|
log.WithField("device", d.Name).Debug("trying to Dial")
|
2018-03-21 02:28:10 +01:00
|
|
|
if !c.enableTLS {
|
2020-02-10 19:55:10 +01:00
|
|
|
if (d.Port) == "" {
|
2019-12-26 19:17:27 +01:00
|
|
|
d.Port = apiPort
|
|
|
|
}
|
|
|
|
conn, err = net.DialTimeout("tcp", d.Address+":"+d.Port, c.timeout)
|
2019-07-03 02:46:19 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// return routeros.DialTimeout(d.Address+apiPort, d.User, d.Password, c.timeout)
|
|
|
|
} else {
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
InsecureSkipVerify: c.insecureTLS,
|
|
|
|
}
|
2020-02-10 19:55:10 +01:00
|
|
|
if (d.Port) == "" {
|
2019-12-26 19:17:27 +01:00
|
|
|
d.Port = apiPortTLS
|
|
|
|
}
|
2019-11-11 23:13:46 +01:00
|
|
|
conn, err = tls.DialWithDialer(&net.Dialer{
|
|
|
|
Timeout: c.timeout,
|
|
|
|
},
|
2019-12-26 19:17:27 +01:00
|
|
|
"tcp", d.Address+":"+d.Port, tlsCfg)
|
2019-07-03 02:46:19 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.WithField("device", d.Name).Debug("done dialing")
|
|
|
|
|
|
|
|
client, err := routeros.NewClient(conn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2018-03-21 02:28:10 +01:00
|
|
|
}
|
2019-07-03 02:46:19 +02:00
|
|
|
log.WithField("device", d.Name).Debug("got client")
|
2018-03-21 02:28:10 +01:00
|
|
|
|
2019-07-03 02:46:19 +02:00
|
|
|
log.WithField("device", d.Name).Debug("trying to login")
|
|
|
|
r, err := client.Run("/login", "=name="+d.User, "=password="+d.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2018-03-21 02:28:10 +01:00
|
|
|
}
|
2019-07-03 02:46:19 +02:00
|
|
|
ret, ok := r.Done.Map["ret"]
|
|
|
|
if !ok {
|
|
|
|
// Login method post-6.43 one stage, cleartext and no challenge
|
|
|
|
if r.Done != nil {
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("RouterOS: /login: no ret (challenge) received")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login method pre-6.43 two stages, challenge
|
|
|
|
b, err := hex.DecodeString(ret)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("RouterOS: /login: invalid ret (challenge) hex string received: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err = client.Run("/login", "=name="+d.User, "=response="+challengeResponse(b, d.Password))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
log.WithField("device", d.Name).Debug("done wth login")
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
|
|
|
|
//tlsCfg := &tls.Config{
|
|
|
|
// InsecureSkipVerify: c.insecureTLS,
|
|
|
|
//}
|
|
|
|
// return routeros.DialTLSTimeout(d.Address+apiPortTLS, d.User, d.Password, tlsCfg, c.timeout)
|
|
|
|
}
|
|
|
|
|
|
|
|
func challengeResponse(cha []byte, password string) string {
|
|
|
|
h := md5.New()
|
|
|
|
h.Write([]byte{0})
|
|
|
|
_, _ = io.WriteString(h, password)
|
|
|
|
h.Write(cha)
|
|
|
|
return fmt.Sprintf("00%x", h.Sum(nil))
|
2017-11-29 05:58:39 +01:00
|
|
|
}
|