#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Tony Boston 2026                   tboston@csitlab.org |
# +------------------------------------------------------------------+
#
# this file is part of mkp package "mikrotik"
# see package description and ~/local/share/doc/check_mk/mikrotik
# for details and maintainer
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3 as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-only


import sys, posix, time, binascii, socket, select, ssl, getopt, hashlib, re

from collections import OrderedDict
from string import ascii_uppercase as alphabet

import pprint

try:
   import requests
except:
   sys.stderr.write("cannot import requests. Try installing package ")
   sys.exit(1)

from requests.packages import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

#
# usage information
#
def usage():
    sys.stderr.write("""CheckMK MikroTik Special Agent

USAGE: agent_mikrotik -u <USER> -p <PASSWORD> [OPTIONS] <HOST>
       agent_mikrotik -h

ARGUMENTS:
  HOST                          Host name or IP address of the target device
  -u USER, --user USER          Username for MikroTik router login
  -p PASS, --pass PASS          Password for MikroTik router login

OPTIONS:
  -h, --help                    Show this help message and exit
  -n, --no-ssl                  Do not use SSL/TLS (RouterOS API and REST)
  -t, --skip-cert-check         Do not make certificate verification
  -r, --rest                    Use RESTful API to connect to device
                                (default: RouterOS API)
  -c PORT, --connect PORT       Connect to this port. The defaults:
                                - 8728 (RouterOS API no SSL)
                                - 8729 (RouterOS API SSL)
                                - 443 (REST API, 80 with --no-ssl)
  --debug                       Debug mode: write some debug messages,
                                let Python exceptions come through
  -i MODULES, --modules MODULES Modules to query. This is a comma separated list
                                which may contain the keywords "bgp", "ospf",
                                "health", "board", "vrrp", "ipsec", "chains",
                                "file", "license", "wifi", "wireguard", "ntp",
                                "pools", "ups", "interfaces" and "system".
                                You can define to use only few of them to optimize
                                performance. The default is "all", which covers
                                every module except "wifi", "wireguard", "ntp",
                                "pools", "ups", "interfaces" and "system"
                                (opt-in, name them explicitly).

""")


# taken from https://wiki.mikrotik.com/wiki/Manual:API_Python3, slightly modified
class ApiRos:
    "Routeros api"
    def __init__(self, sk):
        self.sk = sk
        self.currenttag = 0

    def login(self, username, pwd):
        for repl, attrs in self.talk(["/login", "=name=" + username,
                                      "=password=" + pwd]):
          if repl == '!trap':
            return False
          elif '=ret' in attrs.keys():
            chal = binascii.unhexlify((attrs['=ret']).encode(sys.stdout.encoding))
            md = hashlib.md5()
            md.update(b'\x00')
            md.update(pwd.encode(sys.stdout.encoding))
            md.update(chal)
            for repl2, attrs2 in self.talk(["/login", "=name=" + username,
                   "=response=00" + binascii.hexlify(md.digest()).decode(sys.stdout.encoding) ]):
              if repl2 == '!trap':
                return False
        return True

    def talk(self, words):
        if self.writeSentence(words) == 0: return
        r = []
        while 1:
            i = self.readSentence();
            if len(i) == 0: continue
            reply = i[0]
            attrs = {}
            for w in i[1:]:
                j = w.find('=', 1)
                if (j == -1):
                    attrs[w] = ''
                else:
                    attrs[w[:j]] = w[j+1:]
            r.append((reply, attrs))
            if reply == '!done': return r

    def writeSentence(self, words):
        ret = 0
        for w in words:
            self.writeWord(w)
            ret += 1
        self.writeWord('')
        return ret

    def readSentence(self):
        r = []
        while 1:
            w = self.readWord()
            if w == '': return r
            r.append(w)

    def writeWord(self, w):
        self.writeLen(len(w))
        self.writeStr(w)

    def readWord(self):
        ret = self.readStr(self.readLen())
        return ret

    def writeLen(self, l):
        if l < 0x80:
            self.writeByte((l).to_bytes(1, sys.byteorder))
        elif l < 0x4000:
            l |= 0x8000
            tmp = (l >> 8) & 0xFF
            self.writeByte(((l >> 8) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte((l & 0xFF).to_bytes(1, sys.byteorder))
        elif l < 0x200000:
            l |= 0xC00000
            self.writeByte(((l >> 16) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 8) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte((l & 0xFF).to_bytes(1, sys.byteorder))
        elif l < 0x10000000:
            l |= 0xE0000000
            self.writeByte(((l >> 24) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 16) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 8) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte((l & 0xFF).to_bytes(1, sys.byteorder))
        else:
            self.writeByte((0xF0).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 24) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 16) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte(((l >> 8) & 0xFF).to_bytes(1, sys.byteorder))
            self.writeByte((l & 0xFF).to_bytes(1, sys.byteorder))

    def readLen(self):
        c = ord(self.readStr(1))
        if (c & 0x80) == 0x00:
            pass
        elif (c & 0xC0) == 0x80:
            c &= ~0xC0
            c <<= 8
            c += ord(self.readStr(1))
        elif (c & 0xE0) == 0xC0:
            c &= ~0xE0
            c <<= 8
            c += ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
        elif (c & 0xF0) == 0xE0:
            c &= ~0xF0
            c <<= 8
            c += ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
        elif (c & 0xF8) == 0xF0:
            c = ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
            c <<= 8
            c += ord(self.readStr(1))
        return c

    def writeStr(self, str):
        n = 0;
        while n < len(str):
            r = self.sk.send(bytes(str[n:], 'UTF-8'))
            if r == 0: raise RuntimeError("connection closed by remote end")
            n += r

    def writeByte(self, str):
        n = 0;
        while n < len(str):
            r = self.sk.send(str[n:])
            if r == 0: raise RuntimeError("connection closed by remote end")
            n += r

    def readStr(self, length):
        # thx Ian
        ret = b''
        while len(ret) < length:
            s = self.sk.recv(length - len(ret))
            if s == b'': raise RuntimeError("connection closed by remote end")
            if s >= (128).to_bytes(1, "big") :
               return s
            ret += s
        return ret.decode(sys.stdout.encoding, "replace")

def open_socket(dst, port, secure=True, certcheck=True):
  s = None
  res = socket.getaddrinfo(dst, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
  af, socktype, proto, canonname, sockaddr = res[0]
  skt = socket.socket(af, socktype, proto)
  if secure:
    if certcheck:
      s = ssl.create_default_context()
      s = s.wrap_socket(skt, server_hostname=dst)
    else:
      s = ssl._create_unverified_context()
      s = s.wrap_socket(skt)
  else:
    s = skt
  s.connect(sockaddr)
  return s


# convert list returned by api to a dict
# before:  ['!re', '=.id=*391', '=spi=0xF598639', '=src-address=77.220.238.235', ...]
# after:   {'.id': '*391', 'spi': '0xF598639', 'src-address': '77.220.238.235', ... }
# values may contain '=' themselves (base64 keys, comments), so split twice only
def list2dict(list):
    return {item.split('=', 2)[1]: item.split('=', 2)[2] for item in list if len(item.split('=', 2)) == 3 }

# send command to api and return answer as list of dicts
# readSentence returns one sentence only per connect, therefore need to loop
# just read as long there is any info, then stop reading and return result
def myapi(command):

    result=[]

    # REST API
    if restful:
       agent     = 'CMK Mikrotik Plugin'
       session   = requests.Session()
       headers   = {}

       headers['user-agent'] = agent
       session.auth          = (user, password)

       protocol = 'https' if secure else 'http'
       urlbase = '%s://%s:%s/rest' % (protocol, host, port)
       url     = '%s%s' % (urlbase, command[0].replace('/print',''))

       # credentials
       try:
           auth = session.post(urlbase, verify=certcheck, headers=headers)
       except:
           sys.stderr.write("Connection failed")
           sys.exit(1)

       # get info
       response  = session.get(url, verify=certcheck, headers=headers)
       if response.status_code == 401:
           sys.stderr.write("Unauthorized\n")
           sys.exit(1)

       # will return dict or list
       if type(response.json()) is list:
           result = response.json()
       else:
           result.append(response.json())

       if opt_debug == True:
            print('\n# DEBUG_OUTPUT myapi(%s)' % command)
            pprint.pprint(url)
            pprint.pprint(result)
            print('\n# DEBUG_OUTPUT myapi(%s)' % command)

       return result

    # not restful: RouterOS API
    for step in command:
        apiros.writeSentence([step])
    result=[]
    while True:
        new = apiros.readSentence()
        if '!empty' in new:
            continue
        if len(new) > 1:
            result.append(list2dict(new))
        else:
            if opt_debug == True:
                print('\n# DEBUG_OUTPUT myapi(%s)' % command)
                pprint.pprint(result)
                print('\n# DEBUG_OUTPUT myapi(%s)' % command)
            return result

# run a print command that may not exist on this device (wrong wireless
# driver, no CAPsMAN). Returns None then: the RouterOS API answers with a
# trap ({'message': ...}), the REST API with an error dict.
def wifi_rows(api, command):
    try:
        rows = api([command])
    except Exception:
        return None
    for row in rows:
        if 'error' in row or ('message' in row and 'name' not in row
                              and 'interface' not in row):
            return None
    return rows

# wifi section, pipe separated because SSIDs may contain spaces:
#   iface|<driver>|<interface>|<ssid>|<up|down>
#   client|<driver>|<interface>|<ssid>|<signal dBm>|<band>
#   cap|<driver>|<identity>|<model>|<serial>|<version>|<base mac>|<state>
# Client MAC addresses are not reported on purpose. cap lines only come from a
# CAPsMAN controller, one per managed access point (for the HW/SW inventory).
def wifi_lines(api):
    lines = []
    for driver, iface_command, reg_command, config_command, cap_command in WIFI_SOURCES:
        if cap_command:
            for cap in wifi_rows(api, cap_command) or []:
                lines.append(['cap', driver, cap.get('identity', ''),
                              cap.get('board-name') or cap.get('board', ''),
                              cap.get('serial', ''), cap.get('version', ''),
                              cap.get('base-mac', ''), cap.get('state', '')])

        ifaces = wifi_rows(api, iface_command)
        if ifaces is None:
            continue

        # interfaces provisioned by CAPsMAN often only name a configuration
        # profile (configuration=cfg1) instead of an inline SSID
        profile_ssid = {}
        if config_command:
            for profile in wifi_rows(api, config_command) or []:
                if profile.get('name') and profile.get('ssid'):
                    profile_ssid[profile['name']] = profile['ssid']

        ssid_of = {}
        for iface in ifaces:
            name = iface.get('name')
            if not name or iface.get('disabled') == 'true':
                continue
            ssid = iface.get('configuration.ssid') or iface.get('ssid') \
                   or profile_ssid.get(iface.get('configuration', '')) or ''
            ssid_of[name] = ssid
            # CAP-operated interfaces always report running=false on the
            # controller; there bound + not inactive means the radio is up
            if 'bound' in iface:
                up = iface.get('bound') == 'true' and iface.get('inactive') != 'true'
            else:
                up = iface.get('running') == 'true'
            state = 'up' if up else 'down'
            lines.append(['iface', driver, name, ssid, state])

        for client in wifi_rows(api, reg_command) or []:
            name = client.get('interface', '')
            ssid = client.get('ssid') or ssid_of.get(name, '')
            # wifi: "-56", capsman: "-56", wireless: "-56@HT20-7"
            raw = client.get('signal') or client.get('rx-signal') \
                  or client.get('signal-strength') or ''
            match = re.match(r'-?\d+', raw)
            signal = match.group(0) if match else ''
            lines.append(['client', driver, name, ssid, signal,
                          client.get('band', '')])

    clean = lambda field: field.replace('|', '/').replace('\n', ' ')
    return ['|'.join(clean(field) for field in line) for line in lines]

# wireguard section, pipe separated because comments may contain spaces:
#   peer|<interface>|<name>|<comment>|<public key>|<disabled>|<endpoint>|<last handshake>|<rx bytes>|<tx bytes>
# last handshake is passed on as RouterOS reports it (e.g. 1m9s) and is empty
# if the peer never had a handshake. Older RouterOS 7 releases have no peer name.
def wireguard_lines(api):
    lines = []
    for peer in wifi_rows(api, "/interface/wireguard/peers/print") or []:
        if not peer.get('interface'):
            continue
        address = peer.get('current-endpoint-address') or peer.get('endpoint-address', '')
        port = peer.get('current-endpoint-port') or peer.get('endpoint-port', '')
        endpoint = '%s:%s' % (address, port) if address and port not in ('', '0') else address
        lines.append(['peer', peer['interface'], peer.get('name', ''),
                      peer.get('comment', ''), peer.get('public-key', ''),
                      peer.get('disabled', 'false'), endpoint,
                      peer.get('last-handshake', ''),
                      peer.get('rx', ''), peer.get('tx', '')])

    clean = lambda field: field.replace('|', '/').replace('\n', ' ')
    return ['|'.join(clean(field) for field in line) for line in lines]

# run a command with arguments (e.g. "monitor" with numbers and once) in
# both API modes. myapi() sends every word as a sentence of its own and
# the REST API needs a POST with a JSON body for such commands, so this
# has its own request. Returns a list of dicts, None on error.
def api_call(path, args, timeout=15):
    # timeout: seconds, REST API only. The RouterOS API connection has no
    # per-command timeout; a cut-off answer would leave the connection out
    # of step for the following commands.
    if restful:
        protocol = 'https' if secure else 'http'
        url = '%s://%s:%s/rest%s' % (protocol, host, port, path)
        try:
            response = requests.post(url, json=args, auth=(user, password),
                                     verify=certcheck, timeout=timeout,
                                     headers={'user-agent': 'CMK Mikrotik Plugin'})
            data = response.json()
        except Exception:
            return None
        if opt_debug:
            print('\n# DEBUG_OUTPUT api_call(%s, %s)' % (path, args))
            pprint.pprint(data)
        if response.status_code != 200:
            return None
        return data if isinstance(data, list) else [data]

    words = [path] + ['=%s=%s' % (key, value) for key, value in args.items()]
    try:
        apiros.writeSentence(words)
        result, trapped = [], False
        while True:
            sentence = apiros.readSentence()
            if not sentence or sentence[0] == '!empty':
                continue
            if sentence[0] == '!trap':
                trapped = True
            elif sentence[0] == '!re':
                result.append(list2dict(sentence))
            if sentence[0] in ('!done', '!fatal'):
                break
    except Exception:
        return None
    if opt_debug:
        print('\n# DEBUG_OUTPUT api_call(%s, %s)' % (path, args))
        pprint.pprint(result)
    return None if trapped else result

# system resources from one /system/resource request, as three sections:
#   mikrotik_system         CPU, system disk, NAND bad blocks
#   mikrotik_system_mem     total and free memory (-> Checkmk mem_used)
#   mikrotik_system_uptime  uptime (-> Checkmk uptime)
# Separate sections because memory and uptime feed Checkmk's own parsed
# sections and checks. Key value lines, like the board section.
# cpu-load is a short-term value: the answer of the first request of the
# run is used, taken before the agent's own requests (HTTPS on small CPUs)
# load the device; queried at the time of the module it showed up to 20 %
# instead of 1-4 % on a hEX S.
_SYSTEM_KEYS = ('cpu-load', 'cpu-count', 'cpu', 'cpu-frequency',
                'free-hdd-space', 'total-hdd-space', 'bad-blocks',
                'write-sect-since-reboot', 'write-sect-total')

def system_sections(api, rows=None):
    if not rows:
        rows = wifi_rows(api, "/system/resource/print") or []
    if not rows:
        return []
    res = rows[0]
    out = []
    for header, keys in (('mikrotik_system', _SYSTEM_KEYS),
                         ('mikrotik_system_mem', ('total-memory', 'free-memory')),
                         ('mikrotik_system_uptime', ('uptime',))):
        lines = ['%s %s' % (key, res[key]) for key in keys if key in res]
        if lines:
            out.append('<<<%s>>>' % header)
            out.extend(lines)
            out.append('<<<>>>')
    return out

# negotiated rate of an ethernet port ("1Gbps", "2.5Gbps", "100Mbps") in bit/s
def rate_to_bps(rate):
    match = re.match(r'([\d.]+)\s*([KMG])bps', rate or '')
    if not match:
        return 0
    factor = {'K': 10**3, 'M': 10**6, 'G': 10**9}[match.group(2)]
    return int(float(match.group(1)) * factor)

# network interfaces, pipe separated because comments may contain spaces:
#   <index>|<name>|<type>|<disabled>|<running|not-present>|<dynamic>|<mac>|<speed bit/s>|
#   <rx-byte>|<tx-byte>|<rx-packet>|<tx-packet>|<rx-error>|<tx-error>|
#   <rx-drop>|<tx-drop>|<comment>
# index is the RouterOS internal id (*1A -> 26), the same as ifIndex via
# SNMP and stable when other interfaces are added or removed. speed is only
# known for ethernet/SFP ports: the negotiated rate from
# /interface/ethernet/monitor, not the configured speed. An enabled ethernet
# port without link whose SFP cage is empty is reported as not-present
# (ifOperStatus notPresent via SNMP). With the REST API that monitor call
# has a short timeout, so it cannot delay the rest; with the RouterOS API
# the timeout does not apply (api_call ignores it there, the call returns
# at once because of "once").
def interface_lines(api):
    ifaces = wifi_rows(api, "/interface/print")
    if not ifaces:
        return []

    def ether_ids(running):
        return [i['.id'] for i in ifaces
                if i.get('type') == 'ether' and '.id' in i
                and (i.get('running') == 'true') == running
                and (running or i.get('disabled') != 'true')]

    speed_of = {}
    running_ether = ether_ids(True)
    if running_ether:
        for row in api_call('/interface/ethernet/monitor',
                            {'numbers': ','.join(running_ether), 'once': ''}) or []:
            if row.get('name'):
                speed_of[row['name']] = rate_to_bps(row.get('rate'))

    not_present = set()
    idle_ether = ether_ids(False)
    if idle_ether:
        for row in api_call('/interface/ethernet/monitor',
                            {'numbers': ','.join(idle_ether), 'once': ''},
                            timeout=5) or []:
            if row.get('name') and row.get('sfp-module-present') == 'false':
                not_present.add(row['name'])

    lines = []
    for iface in ifaces:
        name = iface.get('name')
        ident = iface.get('.id', '')
        if not name or not ident.startswith('*'):
            continue
        try:
            index = str(int(ident[1:], 16))
        except ValueError:
            continue
        lines.append([
            index, name, iface.get('type', ''),
            iface.get('disabled', 'false'),
            'not-present' if name in not_present else iface.get('running', 'false'),
            iface.get('dynamic', 'false'), iface.get('mac-address', ''),
            str(speed_of.get(name, 0)),
            iface.get('rx-byte', '0'), iface.get('tx-byte', '0'),
            iface.get('rx-packet', '0'), iface.get('tx-packet', '0'),
            iface.get('rx-error', '0'), iface.get('tx-error', '0'),
            iface.get('rx-drop', '0'), iface.get('tx-drop', '0'),
            iface.get('comment', ''),
        ])

    clean = lambda field: str(field).replace('|', '/').replace('\n', ' ')
    return ['|'.join(clean(field) for field in line) for line in lines]

# UPS (/system/ups, package ups.npk), pipe separated:
#   ups|<name>|<field>|<value>    configuration and identification
#   mon|<name>|<field>|<value>    "/system/ups/monitor once" of this UPS
# No mon lines if the monitor call fails or the UPS is disabled; the check
# then reports that the UPS delivers no data.
_UPS_FIELDS = ('model', 'serial', 'version', 'manufacture-date',
               'nominal-battery-voltage', 'port', 'disabled', 'invalid',
               'offline-time', 'min-runtime')

def ups_lines(api):
    lines = []
    for ups in wifi_rows(api, "/system/ups/print") or []:
        name = ups.get('name') or ups.get('.id')
        if not name:
            continue
        for key in _UPS_FIELDS:
            if key in ups:
                lines.append(['ups', name, key, str(ups[key])])
        if ups.get('disabled', 'false') == 'true' or '.id' not in ups:
            continue
        for row in (api_call('/system/ups/monitor',
                             {'numbers': ups['.id'], 'once': ''}) or [])[:1]:
            for key, value in row.items():
                if key not in ('.id', '.section'):
                    lines.append(['mon', name, key, str(value)])

    clean = lambda field: field.replace('|', '/').replace('\n', ' ')
    return ['|'.join(clean(field) for field in line) for line in lines]

# IP pools, pipe separated (ranges contain commas):
#   pool|<name>|<ranges>|<next-pool>|<used>|<total>|<available>
# Newer RouterOS 7 versions report total, used and available per pool in
# /ip/pool/print; then one request is enough. Otherwise the used addresses
# are counted from /ip/pool/used (one row per lease, the leases themselves
# are not passed on) and total/available stay empty: the check computes the
# size from the ranges. If counting fails, used stays empty as well and the
# check goes UNKNOWN instead of reporting 0 %.
def _count(value):
    value = str(value if value is not None else '').strip()
    return value if value.isdigit() else ''

def pool_lines(api):
    pools = wifi_rows(api, "/ip/pool/print")
    if not pools:
        return []
    used = None
    if any(_count(pool.get('used')) == '' for pool in pools if pool.get('name')):
        rows = wifi_rows(api, "/ip/pool/used/print")
        if rows is not None:
            used = {}
            for row in rows:
                name = row.get('pool', '')
                used[name] = used.get(name, 0) + 1
    lines = []
    for pool in pools:
        name = pool.get('name')
        if not name:
            continue
        count = _count(pool.get('used'))
        if count == '' and used is not None:
            count = str(used.get(name, 0))
        lines.append(['pool', name, pool.get('ranges', ''),
                      pool.get('next-pool', ''), count,
                      _count(pool.get('total')), _count(pool.get('available'))])

    clean = lambda field: field.replace('|', '/').replace('\n', ' ')
    return ['|'.join(clean(field) for field in line) for line in lines]

# command line options
short_options = 'h:u:p:nc:c:i:r:t'
long_options  = [ 'help', 'user=', 'pass=', 'no-ssl', 'connect=', 'debug', 'modules=', 'rest', 'skip-cert-check' ]

try:
    opts, args = getopt.getopt(sys.argv[1:], short_options, long_options)
except (getopt.GetoptError, err):
    sys.stderr.write("%s\n" % err)
    sys.exit(1)

opt_debug       = False
opt_timeout     = 10
opt_any_hostkey = ""
secure          = True
port            = None
restful         = False
certcheck       = True

host            = None
user            = None
password        = None
mortypes        = [ 'all' ]
firewall_show_disabled = False

# make commands api version aware
command_set = {}

# firmware 6
command_set['6'] = {
    "bgp"           : {
                       "header"  : "mikrotik_bgp",
                       "active"  : False,
                       "command" : ["/routing/bgp/peer/print"],
                       "filtered": ['name', 'established', 'remote-as', 'remote-address'],
                       "required": True,
                      },

    "ospf"          : {
                       "header"  : "mikrotik_ospf",
                       "active"  : False,
                       "command" : ["/routing/ospf/neighbor/print"],
                       # router-id must stay first: it starts a new adjacency
                       "filtered": ['router-id', 'instance', 'interface', 'address', 'state'],
                       "required": True,
                      },

    "health"        : {
                       "header"  : "mikrotik_health",
                       "active"  : False,
                       "command" : ["/system/health/print"]
                      },

    "board"         : {
                       "header"  : "mikrotik_board",
                       "active"  : False,
                       "command" : ["/system/resource/print"]
                      },

    "vrrp"          : {
                       "header"  : "mikrotik_vrrp",
                       "active"  : False,
                       "command" : ["/interface/vrrp/print"],
                       "filtered": ['name', 'vrid', 'invalid', 'mac-address', 'master', 
                                    'backup', 'interface', 'running', 'disabled'],
                      },

    "ipsec"         : {
                       "header"  : "mikrotik_ipsec",
                       "active"  : False,
                       "command" : ["/ip/ipsec/peer/print"]
                      },

    "file"          : {
                       "header"  : "mikrotik_file",
                       "active"  : False,
                       "command" : ["/file/print"],
                       "filtered": ['name', 'creation-time', 'last-modified', 'type'],
                       "required": True
                      },

    "firewall"      : {
                       "header"  : "mikrotik_firewall",
                       "active"  : False,
                       "command" : ["/ip/firewall/filter/print"]
                      },
    "license"       : {
                       "header"  : "mikrotik_license",
                       "active"  : False,
                       "command" : ["/system/license/print"]
                      },

    # the commands are in WIFI_SOURCES, the device decides which exist
    "wifi"          : {
                       "header"  : "mikrotik_wifi",
                       "active"  : False,
                       "command" : [],
                      },
}

# modules that "all" does not include - they only run when named in --modules
OPT_IN_MODULES = ("wifi", "wireguard", "ntp", "pools", "ups", "interfaces", "system")

# wifi: one entry per RouterOS wireless driver. Devices only know the menus
# of the installed driver; the others answer with a trap and are skipped.
#   wifi     - new driver (RouterOS 7.13+, also CAPsMAN for it)
#   wireless - legacy driver, local interfaces
#   capsman  - legacy CAPsMAN (/caps-man), clients of all managed CAPs
# (driver, interfaces, registration table, configuration profiles or None,
#  managed access points of a CAPsMAN controller or None)
WIFI_SOURCES = [
    ("wifi",     "/interface/wifi/print",     "/interface/wifi/registration-table/print",
                 "/interface/wifi/configuration/print",
                 "/interface/wifi/capsman/remote-cap/print"),
    ("wireless", "/interface/wireless/print", "/interface/wireless/registration-table/print",
                 None, None),
    ("capsman",  "/caps-man/interface/print", "/caps-man/registration-table/print",
                 "/caps-man/configuration/print", "/caps-man/remote-cap/print"),
]

# IP pools (DHCP, PPP, hotspot), RouterOS 6 and 7; the commands are in
# pool_lines
command_set['6']['pools'] = {
                       "header"   : "mikrotik_pool",
                       "active"   : False,
                       "command"  : [],
                      }

# system resources (CPU, memory, system disk, uptime), RouterOS 6 and 7;
# one /system/resource request, printed as three sections, see system_sections
command_set['6']['system'] = {
                       "header"   : "mikrotik_system",
                       "active"   : False,
                       "command"  : [],
                      }

# network interfaces, RouterOS 6 and 7; the commands are in interface_lines
command_set['6']['interfaces'] = {
                       "header"   : "mikrotik_if",
                       "active"   : False,
                       "command"  : [],
                      }

# UPS (package ups.npk), RouterOS 6 and 7; the commands are in ups_lines
command_set['6']['ups'] = {
                       "header"   : "mikrotik_ups",
                       "active"   : False,
                       "command"  : [],
                      }

# firmware 7 slightly different from 6
command_set['7'] = command_set['6'].copy()
command_set['7']['bgp']= {
                       "header"   : "mikrotik_bgp",
                       "active"   : False,
                       "command"  : ["/routing/bgp/connection/print"],
                       "filtered" : ['name', 'established', 'remote.as', 'remote.address'],
                       "required" : True
                      }

# WireGuard exists since RouterOS 7, the command is in wireguard_lines
command_set['7']['wireguard'] = {
                       "header"   : "mikrotik_wireguard",
                       "active"   : False,
                       "command"  : [],
                      }

# NTP client of RouterOS 7 (status, synced-server, synced-stratum,
# system-offset, freq-drift); printed as key value lines
command_set['7']['ntp'] = {
                       "header"   : "mikrotik_ntp",
                       "active"   : False,
                       "command"  : ["/system/ntp/client/print"],
                      }

for o,a in opts:
    if o in [ '--debug' ]:
        opt_debug = True
    elif o in [ '-u', '--user' ]:
        user = a
    elif o in [ '-p', '--pass' ]:
        # Support CheckMK password store: value arrives as "key:/path/to/stored_passwords"
        if ':' in a:
            _key, _path = a.split(':', 1)
            try:
                import pathlib as _pathlib
                import cmk.utils.password_store as _ps
                password = _ps.lookup(_pathlib.Path(_path), _key)
            except Exception:
                password = a
        else:
            password = a
    elif o in [ '-n', '--no-ssl' ]:
        secure   = False
    elif o in [ '-t', '--skip-cert-check' ]:
        certcheck = False
    elif o in [ '-r', '--rest' ]:
        restful = True
    elif o in [ '-c', '--connect' ]:
        port    = a
    elif o in [ '-i', '--modules' ]:
        mortypes = a.split(',')
        # detect firewall:show-disabled
        new_modules = []
        for m in mortypes:
            if m == "firewall:show-disabled":
                firewall_show_disabled = True
                new_modules.append("firewall")
            else:
                new_modules.append(m)
        mortypes = new_modules
    elif o in [ '-h', '--help' ]:
        usage()
        sys.exit(0)

# Default port from the connection type, after all options are parsed so the
# order of --no-ssl and --rest does not matter.
if port is None:
    if restful:
        port = 443 if secure else 80
    else:
        port = 8729 if secure else 8728

if len(args) == 1:
    host = args[0]

if not args:
    sys.stderr.write("ERROR: No arguments.\n\n")
    usage()
    sys.exit(1)

if host == None:
    sys.stderr.write("ERROR: No host given.\n\n")
    usage()
    sys.exit(1)

if user == None:
    sys.stderr.write("ERROR: No user name given.\n\n")
    usage()
    sys.exit(1)

if password == None:
    sys.stderr.write("ERROR: No password given.\n\n")
    usage()
    sys.exit(1)

# connect to API
if not restful:
    s = open_socket(host, port, secure, certcheck)
    if s is None:
        print ('could not open socket')
        sys.exit(1)

    apiros = ApiRos(s);
    if  not apiros.login(user, password):
        print ('cannot log in')
        sys.exit(1)


# special agent header
print ("<<<check_mk>>>")
print ("Version: 4.7.1-mikrotik_agent")

# detect firmware version and set commands
# the first request of the run: also used by the system module, so its
# cpu-load is measured before the agent's own requests load the CPU
first_resource = myapi(["/system/resource/print"])
version=first_resource[0]['version'].split('.')[0]
try:
    command_options = command_set[version]
except:
    # just take most recent one
    command_options = command_set['7']

# which modules are requested
for module in command_options.keys():
    # "all" leaves out the opt-in modules, they only run when named explicitly
    try:
        if mortypes.index("all") >= 0 and module not in OPT_IN_MODULES:
            command_options[module]["active"] = True
    except ValueError:
        pass

    try:
        if mortypes.index(module) >= 0:
            command_options[module]["active"] = True
    except ValueError:
        pass

# this function prints info
def print_filtered():
    for line in out:

        # reset info block
        info_block = []

        for what in command_options[module]["filtered"]:
           try:
               info_block.append("%s %s" % (what, line[what]))
           except:
               continue

        # boarding completed?
        try:
            if command_options[module]["required"] and len(info_block) == len(command_options[module]["filtered"]):
                print('\n'.join(info_block))
                continue
        except:
            pass
        
        print('\n'.join(info_block))


# fetch information from api, loop thru modules, prepare output for checkmk
for module in command_options.keys():

    # to be done?
    if command_options[module]["active"] != True:
        continue

    # wifi queries several menus and needs its own separator
    if module == "wifi":
        lines = wifi_lines(myapi)
        if lines:
            print('<<<mikrotik_wifi:sep(124)>>>')
            print('\n'.join(lines))
            print('<<<>>>')
        continue

    if module == "wireguard":
        lines = wireguard_lines(myapi)
        if lines:
            print('<<<mikrotik_wireguard:sep(124)>>>')
            print('\n'.join(lines))
            print('<<<>>>')
        continue

    # system resources: /system/resource as three sections, see system_sections
    if module == "system":
        lines = system_sections(myapi, first_resource)
        if lines:
            print('\n'.join(lines))
        continue

    # network interfaces: /interface and the ethernet monitor, see interface_lines
    if module == "interfaces":
        lines = interface_lines(myapi)
        if lines:
            print('<<<mikrotik_if:sep(124)>>>')
            print('\n'.join(lines))
            print('<<<>>>')
        continue

    # UPS: /system/ups and "monitor once" per UPS, see ups_lines
    if module == "ups":
        lines = ups_lines(myapi)
        if lines:
            print('<<<mikrotik_ups:sep(124)>>>')
            print('\n'.join(lines))
            print('<<<>>>')
        continue

    # IP pools: /ip/pool and the count of /ip/pool/used, see pool_lines
    if module == "pools":
        lines = pool_lines(myapi)
        if lines:
            print('<<<mikrotik_pool:sep(124)>>>')
            print('\n'.join(lines))
            print('<<<>>>')
        continue

    # read and execute api command
    command = command_options[module]["command"]
    out     = myapi(command)


    # any result?
    if not out:
        continue

    # declare
    header  = 'mikrotik_%s' % module
    print ('<<<%s>>>' % header)

    # these are very similar:
    if module == "bgp" or module == "ospf" or module == "vrrp" or module == 'file':

        # BGP from a RouterOS V6 devivce is already complete
        # newer OS we have to inject info from another api call
        if version == '7' and module == "bgp":
            command = ["/routing/bgp/session/print"]
            bgpout = myapi(command)

            # correlation is kinda tricky: 'name' ist suffixed in second call
            # 1. loop thru first api call, this is connection config
            # 2. then find a session info with a name starting with connection name
            # 3. take established from there (which is missing if not established)
            i = 0
            for line in out:
                for bgpline in bgpout:
                    try:
                        if bgpline['name'].startswith(line['name']):
                            out[i]['established'] = bgpline['established']
                            bgpout.remove(bgpline)
                    except KeyError:
                        out[i]['established'] = 'false'
                i += 1

        # tell the others
        print_filtered()

    # split health into different checks
    elif header == "mikrotik_health":

        # rewrite from version 7
        if version == '7':
            newout = {}
            #print(out)
            for item in list(out):
                
                if 'name' not in item:
                    continue

                key = item['name']
                val = float(item['value'])*1000 if item['type'] == 'A' else item['value']
                newout[key] = val

            out = []
            out.append(newout)

        # use these as (string-)filter for output _and_ check declaration
        health_items = ['fan', 'temp', 'power', 'psu']

        # sort all information to health_items, just compare names
        info = {}
        for line in out:
            for what in health_items:
                info[what] = dict(filter(lambda item: what in item[0], line.items()))


        # special handling if name is not sufficiant
        for special in ['current', 'voltage']:
            info['power'].update(dict(filter(lambda item: special in item[0],
                                                 line.items())))

        # fake temperature to be a linux sensor
        print('<<<lnx_thermal:sep(124)>>>')
        for what in (info['temp']):
            if 'temperature' in what:
                print("%s|enabled|x86_pkg_temp|%s000|0|passive|0|passive" % (
                       what, info['temp'][what]))

        # print all health_items that contain values, skip some
        for what in health_items:
            if what in ['temp']:
                continue

            if info[what]:
                print ("<<<mikrotik_%s>>>" % what)
                for item in info[what]:
                    print ("%s %s" % ( item, info[what][item]))

        # PoE output consumption as its own dedicated section
        if 'poe-out-consumption' in line:
            print("<<<mikrotik_poe>>>")
            print("poe-out-consumption %s" % line['poe-out-consumption'])
            print("<<<>>>")

    # ipsec
    elif header == "mikrotik_ipsec":

        # break on any error (unexpected API response)
        try:
            for line in out:
                print ("peer %s %s %s" % (
                        line['name'], 
                        line['local-address'].split('/')[0], 
                        line['address'].split('/')[0]))

            command = ["/ip/address/print"]
            for line in myapi(command):
                if line['invalid'] == 'true':
                    print ("invip %s %s" % (
                            line['address'].split('/')[0], 
                            line['actual-interface']))

            # ipsec main info: both api calls before are only to detect a standby gateway
            command = ["/ip/ipsec/installed-sa/print"]
            for line in myapi(command):
                try:
                    print ("sa %s %s %s %s %s" % (
                            line['src-address'], 
                            line['dst-address'], 
                            line['state'], 
                            line['current-bytes'], 
                            line['current-packets']))
                except:
                    print ("sa %s %s %s 0 0" % (
                            line['src-address'], 
                            line['dst-address'], 
                            line['state']))
        except:
            print ('<<<>>>')

    # chains
    elif header == "mikrotik_firewall":

        # print global flag if enabled
        if firewall_show_disabled:
            print("@show_disabled true")
        # remove duplicates, that are reported by api for whatever reason
        # - convert list of dicts into list of tuples containing dict
        # - hash (set comprehension)
        # - recreate dict
        out = [dict(t) for t in {tuple(d.items()) for d in out}]

        # print the information we have, do not stop on missing ones
        for line in out:
             for what in ['comment', '.id', 'chain', 'bytes', 'packets', 'disabled']:
                 try:
                     print ("%s %s" % (what, line[what]))
                 except:
                     print ("%s None" % (what))

    # board: /system/resource has version and board-name only,
    # serial and RouterBOOT firmware come from /system/routerboard
    elif header == "mikrotik_board":
        for line in out:
            for item in line:
                print ("%s %s" % ( item, line[item]))

        # prefixed with rb- so the keys cannot clash with /system/resource.
        # Devices without a RouterBOARD (CHR, x86) may lack these fields,
        # so print only what is there and never fail the whole agent on it.
        try:
            rbout = myapi(["/system/routerboard/print"])
        except Exception:
            rbout = []
        for line in rbout:
            for what in ['routerboard', 'model', 'serial-number', 'firmware-type',
                         'current-firmware', 'upgrade-firmware']:
                if what in line:
                    print ("rb-%s %s" % (what, line[what]))

    # any other key-value information with no special handling
    else:
        for line in out:
            for item in line:
                print ("%s %s" % ( item, line[item]))


    print ('<<<>>>')
# This is the end, my friend.

