mirror of
https://github.com/nshttpd/mikrotik-exporter.git
synced 2024-09-30 04:07:52 +02:00
d723fdfe38
* Added support to auto discovery witih SRV The yaml configuration has been modified to suport srv parameters. Now the name is taken form the router identity to allow dynamic discovery of devices based on SRV registry. * Corrected format * feat(collector): Added support to use custom DNS * feat(collector): get default dns server from resolv.conf * Remove getIdentity on the static config devices
400 lines
9.2 KiB
Go
400 lines
9.2 KiB
Go
package collector
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"mikrotik-exporter/config"
|
|
|
|
"github.com/miekg/dns"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
log "github.com/sirupsen/logrus"
|
|
routeros "gopkg.in/routeros.v2"
|
|
)
|
|
|
|
const (
|
|
namespace = "mikrotik"
|
|
apiPort = "8728"
|
|
apiPortTLS = "8729"
|
|
dnsPort = 53
|
|
|
|
// DefaultTimeout defines the default timeout when connecting to a router
|
|
DefaultTimeout = 5 * time.Second
|
|
)
|
|
|
|
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,
|
|
)
|
|
)
|
|
|
|
type collector struct {
|
|
devices []config.Device
|
|
collectors []routerOSCollector
|
|
timeout time.Duration
|
|
enableTLS bool
|
|
insecureTLS bool
|
|
}
|
|
|
|
// WithBGP enables BGP routing metrics
|
|
func WithBGP() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newBGPCollector())
|
|
}
|
|
}
|
|
|
|
// WithRoutes enables routing table metrics
|
|
func WithRoutes() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newRoutesCollector())
|
|
}
|
|
}
|
|
|
|
// WithDHCP enables DHCP serrver metrics
|
|
func WithDHCP() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newDHCPCollector())
|
|
}
|
|
}
|
|
|
|
// WithDHCPL enables DHCP server leases
|
|
func WithDHCPL() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newDHCPLCollector())
|
|
}
|
|
}
|
|
|
|
// WithDHCPv6 enables DHCPv6 serrver metrics
|
|
func WithDHCPv6() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newDHCPv6Collector())
|
|
}
|
|
}
|
|
|
|
// WithPOE enables PoE metrics
|
|
func WithPOE() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newPOECollector())
|
|
}
|
|
}
|
|
|
|
// WithPools enables IP(v6) pool metrics
|
|
func WithPools() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newPoolCollector())
|
|
}
|
|
}
|
|
|
|
// WithOptics enables optical diagnstocs
|
|
func WithOptics() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newOpticsCollector())
|
|
}
|
|
}
|
|
|
|
// WithW60G enables w60g metrics
|
|
func WithW60G() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, neww60gInterfaceCollector())
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|
|
|
|
// WithMonitor enables ethernet monitor collector metrics
|
|
func Monitor() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newMonitorCollector())
|
|
}
|
|
}
|
|
|
|
// WithTimeout sets timeout for connecting to router
|
|
func WithTimeout(d time.Duration) Option {
|
|
return func(c *collector) {
|
|
c.timeout = d
|
|
}
|
|
}
|
|
|
|
// WithTLS enables TLS
|
|
func WithTLS(insecure bool) Option {
|
|
return func(c *collector) {
|
|
c.enableTLS = true
|
|
c.insecureTLS = true
|
|
}
|
|
}
|
|
|
|
// WithIpsec enables ipsec metrics
|
|
func WithIpsec() Option {
|
|
return func(c *collector) {
|
|
c.collectors = append(c.collectors, newIpsecCollector())
|
|
}
|
|
}
|
|
|
|
// Option applies options to collector
|
|
type Option func(*collector)
|
|
|
|
// NewCollector creates a collector instance
|
|
func NewCollector(cfg *config.Config, opts ...Option) (prometheus.Collector, error) {
|
|
log.WithFields(log.Fields{
|
|
"numDevices": len(cfg.Devices),
|
|
}).Info("setting up collector for devices")
|
|
|
|
c := &collector{
|
|
devices: cfg.Devices,
|
|
timeout: DefaultTimeout,
|
|
collectors: []routerOSCollector{
|
|
newInterfaceCollector(),
|
|
newResourceCollector(),
|
|
},
|
|
}
|
|
|
|
for _, o := range opts {
|
|
o(c)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Describe implements the prometheus.Collector interface.
|
|
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
|
|
ch <- scrapeDurationDesc
|
|
ch <- scrapeSuccessDesc
|
|
|
|
for _, co := range c.collectors {
|
|
co.describe(ch)
|
|
}
|
|
}
|
|
|
|
// Collect implements the prometheus.Collector interface.
|
|
func (c *collector) Collect(ch chan<- prometheus.Metric) {
|
|
wg := sync.WaitGroup{}
|
|
|
|
var realDevices []config.Device
|
|
|
|
for _, dev := range c.devices {
|
|
if (config.SrvRecord{}) != dev.Srv {
|
|
log.WithFields(log.Fields{
|
|
"SRV": dev.Srv.Record,
|
|
}).Info("SRV configuration detected")
|
|
conf, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
|
|
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 {
|
|
go func(d config.Device) {
|
|
c.collectForDevice(d, ch)
|
|
wg.Done()
|
|
}(dev)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (c *collector) collectForDevice(d config.Device, ch chan<- prometheus.Metric) {
|
|
begin := time.Now()
|
|
|
|
err := c.connectAndCollect(&d, ch)
|
|
|
|
duration := time.Since(begin)
|
|
var success float64
|
|
if err != nil {
|
|
log.Errorf("ERROR: %s collector failed after %fs: %s", d.Name, duration.Seconds(), err)
|
|
success = 0
|
|
} else {
|
|
log.Debugf("OK: %s collector succeeded after %fs.", d.Name, duration.Seconds())
|
|
success = 1
|
|
}
|
|
|
|
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 {
|
|
ctx := &collectorContext{ch, d, cl}
|
|
err = co.collect(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *collector) connect(d *config.Device) (*routeros.Client, error) {
|
|
var conn net.Conn
|
|
var err error
|
|
|
|
log.WithField("device", d.Name).Debug("trying to Dial")
|
|
if !c.enableTLS {
|
|
if(d.Port) == "" {
|
|
d.Port = apiPort
|
|
}
|
|
conn, err = net.DialTimeout("tcp", d.Address+":"+d.Port, c.timeout)
|
|
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,
|
|
}
|
|
if(d.Port) == "" {
|
|
d.Port = apiPortTLS
|
|
}
|
|
conn, err = tls.DialWithDialer(&net.Dialer{
|
|
Timeout: c.timeout,
|
|
},
|
|
"tcp", d.Address+":"+d.Port, tlsCfg)
|
|
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
|
|
}
|
|
log.WithField("device", d.Name).Debug("got client")
|
|
|
|
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
|
|
}
|
|
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))
|
|
}
|