First implementation
This commit is contained in:
commit
15bb93d8a4
14 changed files with 765 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
coverage.xml
|
||||||
|
junit.xml
|
22
.pre-commit-config.yaml
Normal file
22
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.4.0 # Use the ref you want to point at
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: 6.1.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
|
||||||
|
- repo: https://github.com/ambv/black
|
||||||
|
rev: 23.7.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
33
README.md
Normal file
33
README.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# OPNSense Prometheus exporter
|
||||||
|
|
||||||
|
I've configures OPNSense with High Availability settings using 2 servers.
|
||||||
|
|
||||||
|
* https://docs.opnsense.org/manual/hacarp.html
|
||||||
|
* https://docs.opnsense.org/manual/how-tos/carp.html
|
||||||
|
|
||||||
|
So I've 2 servers: *MAIN* and *BACKUP*, in normal situation *MAIN* server
|
||||||
|
is expected to be `active` and the *BACKUP* server to be in `hot_standby` state.
|
||||||
|
|
||||||
|
|
||||||
|
The initial needs was to be able to make sure that *BACKUP* server is ready (hot standby)
|
||||||
|
to get the main server role with the `active` state at any time.
|
||||||
|
|
||||||
|
> Unfortunately I've not found a proper configuration to call OPNSense HTTP API over
|
||||||
|
> opnvpn on backup server using blackbox configuratoin. That why I've started to develop
|
||||||
|
> this exporter install on a server on the LAN to be able to resquest both OPNSense servers.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
This exporter gives following metrics:
|
||||||
|
|
||||||
|
* ``:
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
> *Note*: Most updated documentation from command line !
|
||||||
|
|
||||||
|
```
|
||||||
|
opnsense-exporter --help
|
||||||
|
```
|
9
opnsense_exporter/__init__.py
Normal file
9
opnsense_exporter/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# this is a namespace package
|
||||||
|
try:
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
pkg_resources.declare_namespace(__name__)
|
||||||
|
except ImportError:
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
__path__ = pkgutil.extend_path(__path__, __name__)
|
90
opnsense_exporter/opnsense_api.py
Normal file
90
opnsense_exporter/opnsense_api.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OPNSenseAPI:
|
||||||
|
host: str = None
|
||||||
|
login: str = None
|
||||||
|
password: str = None
|
||||||
|
|
||||||
|
def __init__(self, host, login, password):
|
||||||
|
self.host = host
|
||||||
|
self.login = login
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def prepare_url(self, path):
|
||||||
|
return f"https://{self.host}{path}"
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
response = requests.get(
|
||||||
|
self.prepare_url(path),
|
||||||
|
auth=(self.login, self.password),
|
||||||
|
timeout=0.5,
|
||||||
|
# # as today I'm using the opnsense selfsigned certificat
|
||||||
|
# # but we should avoid this instead trust any certificat
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_interface_vip_status(self):
|
||||||
|
try:
|
||||||
|
data = self.get("/api/diagnostics/interface/get_vip_status/")
|
||||||
|
except RequestException as ex:
|
||||||
|
logger.error(
|
||||||
|
"Get VIP STATUS on %s failed with the following error %r", self.host, ex
|
||||||
|
)
|
||||||
|
return "unavailable"
|
||||||
|
if data["carp"]["maintenancemode"]:
|
||||||
|
return "maintenancemode"
|
||||||
|
is_active = all([row["status"] == "MASTER" for row in data["rows"]])
|
||||||
|
if is_active:
|
||||||
|
return "active"
|
||||||
|
is_backup = all([row["status"] == "BACKUP" for row in data["rows"]])
|
||||||
|
if is_backup:
|
||||||
|
return "hot_standby"
|
||||||
|
logger.warning(
|
||||||
|
"this host %s is no active nor backup received payload %s", self.host, data
|
||||||
|
)
|
||||||
|
return "unavailable"
|
||||||
|
|
||||||
|
def get_wan_trafic(self):
|
||||||
|
try:
|
||||||
|
data = self.get("/api/diagnostics/traffic/interface")
|
||||||
|
except RequestException as ex:
|
||||||
|
logger.error(
|
||||||
|
"Get diagnostics traffic on WAN interface for %s host failed with the following error %r",
|
||||||
|
self.host,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
return None, None
|
||||||
|
return (
|
||||||
|
int(data["interfaces"]["wan"]["bytes received"]),
|
||||||
|
int(data["interfaces"]["wan"]["bytes transmitted"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# def get_server_system_status(self):
|
||||||
|
# # https://192.168.200.1/api/core/system/status
|
||||||
|
# return {
|
||||||
|
# "CrashReporter":
|
||||||
|
# {
|
||||||
|
# "statusCode":2,
|
||||||
|
# "message":"No problems were detected.",
|
||||||
|
# "logLocation":"/crash_reporter.php",
|
||||||
|
# "timestamp":"0",
|
||||||
|
# "status":"OK"
|
||||||
|
# },
|
||||||
|
# "Firewall":{
|
||||||
|
# "statusCode":2,
|
||||||
|
# "message":"No problems were detected.",
|
||||||
|
# "logLocation":"/ui/diagnostics/log/core/firewall",
|
||||||
|
# "timestamp":"0",
|
||||||
|
# "status":"OK"
|
||||||
|
# },
|
||||||
|
# "System":{"status":"OK"}
|
||||||
|
# }
|
118
opnsense_exporter/server.py
Normal file
118
opnsense_exporter/server.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from prometheus_client import Enum, Gauge, start_http_server
|
||||||
|
|
||||||
|
from opnsense_exporter.opnsense_api import OPNSenseAPI
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
HA_STATES = ["active", "hot_standby", "unavailable", "maintenancemode"]
|
||||||
|
main_ha_state = Enum(
|
||||||
|
"opnsense_main_ha_state", "OPNSense HA state of the MAIN server", states=HA_STATES
|
||||||
|
)
|
||||||
|
backup_ha_state = Enum(
|
||||||
|
"opnsense_backup_ha_state",
|
||||||
|
"OPNSense HA state of the BACKUP server",
|
||||||
|
states=HA_STATES,
|
||||||
|
)
|
||||||
|
active_server_bytes_received = Gauge(
|
||||||
|
"opnsense_active_server_bytes_received",
|
||||||
|
"Active OPNSense server bytes received on WAN interface",
|
||||||
|
)
|
||||||
|
active_server_bytes_transmitted = Gauge(
|
||||||
|
"opnsense_active_server_bytes_transmitted",
|
||||||
|
"Active OPNSense server bytes transmitted on WAN interface",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_requests(main, backup):
|
||||||
|
"""A dummy function that takes some time."""
|
||||||
|
main_state = main.get_interface_vip_status()
|
||||||
|
backup_sate = backup.get_interface_vip_status()
|
||||||
|
main_ha_state.state(main_state)
|
||||||
|
backup_ha_state.state(backup_sate)
|
||||||
|
bytes_received = None
|
||||||
|
bytes_transmitted = None
|
||||||
|
if main_state == "active":
|
||||||
|
bytes_received, bytes_transmitted = main.get_wan_trafic()
|
||||||
|
if backup_sate == "active":
|
||||||
|
bytes_received, bytes_transmitted = backup.get_wan_trafic()
|
||||||
|
active_server_bytes_received.set(bytes_received or -1)
|
||||||
|
active_server_bytes_transmitted.set(bytes_transmitted or -1)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(main: OPNSenseAPI, backup: OPNSenseAPI, check_frequency: int = 1):
|
||||||
|
# Start up the server to expose the metrics.
|
||||||
|
start_http_server(8000)
|
||||||
|
# Generate some requests.
|
||||||
|
while True:
|
||||||
|
process_requests(main, backup)
|
||||||
|
time.sleep(check_frequency)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="OPNSense prometheus exporter",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--check-frequency-seconds",
|
||||||
|
"-c",
|
||||||
|
type=int,
|
||||||
|
dest="frequency",
|
||||||
|
default=int(os.environ.get("CHECK_FREQUENCY_SECONDS", 2)),
|
||||||
|
help="How often (in seconds) this server requests OPNSense servers",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--main-host",
|
||||||
|
"-m",
|
||||||
|
type=str,
|
||||||
|
dest="main",
|
||||||
|
default=os.environ.get("OPNSENSE_MAIN_HOST", None),
|
||||||
|
help="MAIN OPNsense server that should be in `active` state in normal configuration.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--backup-host",
|
||||||
|
"-b",
|
||||||
|
type=str,
|
||||||
|
dest="backup",
|
||||||
|
default=os.environ.get("OPNSENSE_BACKUP_HOST", None),
|
||||||
|
help="BACKUP OPNsense server that should be `hot_standby` state in normal configuration.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--opnsense-user",
|
||||||
|
"-u",
|
||||||
|
type=str,
|
||||||
|
dest="user",
|
||||||
|
default=os.environ.get("OPNSENSE_USERNAME", None),
|
||||||
|
help="OPNsense user. Expect to be the same on MAIN and BACKUP servers",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--opnsense-password",
|
||||||
|
"-p",
|
||||||
|
type=str,
|
||||||
|
dest="password",
|
||||||
|
default=os.environ.get("OPNSENSE_PASSWORD", None),
|
||||||
|
help="OPNsense password. Expect to be the same on MAIN and BACKUP servers",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--prometheus-instance",
|
||||||
|
dest="prom_instance",
|
||||||
|
type=str,
|
||||||
|
default=socket.gethostname(),
|
||||||
|
help=(
|
||||||
|
"Instance name, default value computed with hostname "
|
||||||
|
"where the server is running. Use to set the instance label."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
start_server(
|
||||||
|
OPNSenseAPI(arguments.main, arguments.user, arguments.password),
|
||||||
|
OPNSenseAPI(arguments.backup, arguments.user, arguments.password),
|
||||||
|
check_frequency=arguments.frequency,
|
||||||
|
)
|
3
requirements.tests.txt
Normal file
3
requirements.tests.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
responses
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
prometheus-client
|
||||||
|
python-dotenv
|
||||||
|
requests
|
25
setup.cfg
Normal file
25
setup.cfg
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[isort]
|
||||||
|
profile = black
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 92
|
||||||
|
exclude = log/*,doc/*,*.egg-info
|
||||||
|
max-complexity = 12
|
||||||
|
ignore =
|
||||||
|
# line length is handled by black
|
||||||
|
E501
|
||||||
|
# line break before binary operator (black move the line breaker before)
|
||||||
|
W503
|
||||||
|
per-file-ignores =
|
||||||
|
# tests doesn't require doctrings
|
||||||
|
test_*: D103, W605
|
||||||
|
# empty init doesn't need a docstring
|
||||||
|
# ignore unused imported in init files
|
||||||
|
__init__.py:
|
||||||
|
D104
|
||||||
|
F401
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
addopts = -v -s --junit-xml junit.xml --cov ./opnsense_exporter/ --cov-report term --cov-report xml --cov-report html
|
||||||
|
testpaths =
|
||||||
|
tests
|
39
setup.py
Normal file
39
setup.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_requirements(file):
|
||||||
|
required = []
|
||||||
|
with open(file) as f:
|
||||||
|
for req in f.read().splitlines():
|
||||||
|
if req.strip().startswith("git"):
|
||||||
|
req = urlparse(req.strip()).fragment.split("=")[1]
|
||||||
|
if req.strip().startswith("-e"):
|
||||||
|
req = urlparse(req.strip().split()[1]).fragment.split("=")[1]
|
||||||
|
if not req.strip().startswith("#") and not req.strip().startswith("--"):
|
||||||
|
required.append(req)
|
||||||
|
return required
|
||||||
|
|
||||||
|
|
||||||
|
requires = parse_requirements("requirements.txt")
|
||||||
|
tests_requires = parse_requirements("requirements.tests.txt")
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="opnsense-prom-exporter",
|
||||||
|
version=version,
|
||||||
|
description="OPNSense Prometheus exporter",
|
||||||
|
author="Pierre Verkest",
|
||||||
|
author_email="pierreverkest84@gmail.com",
|
||||||
|
license="GNU GPL v3",
|
||||||
|
namespace_packages=["opnsense_exporter"],
|
||||||
|
packages=find_packages(exclude=["ez_setup", "examples", "tests"]),
|
||||||
|
install_requires=requires,
|
||||||
|
tests_require=requires + tests_requires,
|
||||||
|
entry_points="""
|
||||||
|
[console_scripts]
|
||||||
|
opnsense-exporter=opnsense_exporter.server:run
|
||||||
|
""",
|
||||||
|
)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
149
tests/common.py
Normal file
149
tests/common.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
MAIN_HOST = "192.168.1.1"
|
||||||
|
BACKUP_HOST = "192.168.1.2"
|
||||||
|
LOGIN = "user"
|
||||||
|
PASSWORD = "pwd"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_get_vip_status_paylaod(state_wan, state_lan, maintenance_mode):
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"rowCount": 2,
|
||||||
|
"current": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"interface": "wan",
|
||||||
|
"vhid": "1",
|
||||||
|
"advbase": "1",
|
||||||
|
"advskew": "0",
|
||||||
|
"subnet": "176.149.171.241",
|
||||||
|
"status": state_wan,
|
||||||
|
"mode": "carp",
|
||||||
|
"status_txt": state_wan,
|
||||||
|
"vhid_txt": "1 (freq. 1/0)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "lan",
|
||||||
|
"vhid": "3",
|
||||||
|
"advbase": "1",
|
||||||
|
"advskew": "0",
|
||||||
|
"subnet": "192.168.200.1",
|
||||||
|
"status": state_lan,
|
||||||
|
"mode": "carp",
|
||||||
|
"status_txt": state_lan,
|
||||||
|
"vhid_txt": "3 (freq. 1/0)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"carp": {
|
||||||
|
"demotion": "0",
|
||||||
|
"allow": "1",
|
||||||
|
"maintenancemode": maintenance_mode,
|
||||||
|
"status_msg": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_diagnostics_traffic_interface_paylaod():
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"interfaces": {
|
||||||
|
"lan": {
|
||||||
|
"index": "2",
|
||||||
|
"flags": "8963",
|
||||||
|
"promiscuous listeners": "1",
|
||||||
|
"send queue length": "0",
|
||||||
|
"send queue max length": "50",
|
||||||
|
"send queue drops": "0",
|
||||||
|
"type": "Ethernet",
|
||||||
|
"address length": "6",
|
||||||
|
"header length": "18",
|
||||||
|
"link state": "2",
|
||||||
|
"vhid": "0",
|
||||||
|
"datalen": "152",
|
||||||
|
"mtu": "1500",
|
||||||
|
"metric": "0",
|
||||||
|
"line rate": "10000000000 bit/s",
|
||||||
|
"packets received": "3699327747",
|
||||||
|
"input errors": "0",
|
||||||
|
"packets transmitted": "8972963403",
|
||||||
|
"output errors": "0",
|
||||||
|
"collisions": "0",
|
||||||
|
"bytes received": "2474843996609",
|
||||||
|
"bytes transmitted": "11711737078752",
|
||||||
|
"multicasts received": "29274204",
|
||||||
|
"multicasts transmitted": "0",
|
||||||
|
"input queue drops": "0",
|
||||||
|
"packets for unknown protocol": "0",
|
||||||
|
"HW offload capabilities": "0x0",
|
||||||
|
"uptime at attach or stat reset": "1",
|
||||||
|
"name": "LAN",
|
||||||
|
},
|
||||||
|
"opt1": {
|
||||||
|
"index": "3",
|
||||||
|
"flags": "8863",
|
||||||
|
"promiscuous listeners": "0",
|
||||||
|
"send queue length": "0",
|
||||||
|
"send queue max length": "50",
|
||||||
|
"send queue drops": "0",
|
||||||
|
"type": "Ethernet",
|
||||||
|
"address length": "6",
|
||||||
|
"header length": "18",
|
||||||
|
"link state": "2",
|
||||||
|
"vhid": "0",
|
||||||
|
"datalen": "152",
|
||||||
|
"mtu": "1500",
|
||||||
|
"metric": "0",
|
||||||
|
"line rate": "10000000000 bit/s",
|
||||||
|
"packets received": "1120457",
|
||||||
|
"input errors": "0",
|
||||||
|
"packets transmitted": "178007891",
|
||||||
|
"output errors": "0",
|
||||||
|
"collisions": "0",
|
||||||
|
"bytes received": "221674964",
|
||||||
|
"bytes transmitted": "55808795987",
|
||||||
|
"multicasts received": "393767",
|
||||||
|
"multicasts transmitted": "0",
|
||||||
|
"input queue drops": "0",
|
||||||
|
"packets for unknown protocol": "0",
|
||||||
|
"HW offload capabilities": "0x0",
|
||||||
|
"uptime at attach or stat reset": "1",
|
||||||
|
"name": "LANOPNSYNC",
|
||||||
|
},
|
||||||
|
"wan": {
|
||||||
|
"index": "1",
|
||||||
|
"flags": "8963",
|
||||||
|
"promiscuous listeners": "1",
|
||||||
|
"send queue length": "0",
|
||||||
|
"send queue max length": "50",
|
||||||
|
"send queue drops": "0",
|
||||||
|
"type": "Ethernet",
|
||||||
|
"address length": "6",
|
||||||
|
"header length": "18",
|
||||||
|
"link state": "2",
|
||||||
|
"vhid": "0",
|
||||||
|
"datalen": "152",
|
||||||
|
"mtu": "1500",
|
||||||
|
"metric": "0",
|
||||||
|
"line rate": "10000000000 bit/s",
|
||||||
|
"packets received": "9008014155",
|
||||||
|
"input errors": "0",
|
||||||
|
"packets transmitted": "3724630846",
|
||||||
|
"output errors": "0",
|
||||||
|
"collisions": "0",
|
||||||
|
"bytes received": "11725192686820",
|
||||||
|
"bytes transmitted": "2489262014203",
|
||||||
|
"multicasts received": "2288794",
|
||||||
|
"multicasts transmitted": "0",
|
||||||
|
"input queue drops": "0",
|
||||||
|
"packets for unknown protocol": "0",
|
||||||
|
"HW offload capabilities": "0x0",
|
||||||
|
"uptime at attach or stat reset": "1",
|
||||||
|
"name": "WAN",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"time": 1693404742.796736,
|
||||||
|
}
|
||||||
|
)
|
106
tests/test_opnsense_api.py
Normal file
106
tests/test_opnsense_api.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from opnsense_exporter.opnsense_api import OPNSenseAPI
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
LOGIN,
|
||||||
|
MAIN_HOST,
|
||||||
|
PASSWORD,
|
||||||
|
generate_diagnostics_traffic_interface_paylaod,
|
||||||
|
generate_get_vip_status_paylaod,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_interface_vip_status_active():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status() == "active"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_interface_vip_status_backup():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("BACKUP", "BACKUP", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
|
||||||
|
== "hot_standby"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_interface_vip_status_mainteance_mode():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", True),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
|
||||||
|
== "maintenancemode"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_interface_vip_status_unavailable_weird_case():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "BACKUP", False),
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
|
||||||
|
== "unavailable"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_interface_vip_status_unavailable_rest_api_error():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
json={"error": "not found"},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
|
||||||
|
== "unavailable"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_wan_traffic():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/traffic/interface",
|
||||||
|
body=generate_diagnostics_traffic_interface_paylaod(),
|
||||||
|
)
|
||||||
|
assert OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_wan_trafic() == (
|
||||||
|
11725192686820,
|
||||||
|
2489262014203,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_wan_traffic_none():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/traffic/interface",
|
||||||
|
json={"error": "not found"},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
assert OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_wan_trafic() == (
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
165
tests/test_server.py
Normal file
165
tests/test_server.py
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from opnsense_exporter.opnsense_api import OPNSenseAPI
|
||||||
|
from opnsense_exporter.server import process_requests, run
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
BACKUP_HOST,
|
||||||
|
LOGIN,
|
||||||
|
MAIN_HOST,
|
||||||
|
PASSWORD,
|
||||||
|
generate_diagnostics_traffic_interface_paylaod,
|
||||||
|
generate_get_vip_status_paylaod,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("opnsense_exporter.server.start_server")
|
||||||
|
def test_parser(server_mock):
|
||||||
|
with mock.patch(
|
||||||
|
"sys.argv",
|
||||||
|
[
|
||||||
|
"opnsense-exporter",
|
||||||
|
"-c",
|
||||||
|
"15",
|
||||||
|
"-m",
|
||||||
|
"main.host",
|
||||||
|
"-b",
|
||||||
|
"backup.host",
|
||||||
|
"-u",
|
||||||
|
"user-test",
|
||||||
|
"-p",
|
||||||
|
"pwd-test",
|
||||||
|
"--prometheus-instance",
|
||||||
|
"server-hostname-instance",
|
||||||
|
],
|
||||||
|
):
|
||||||
|
run()
|
||||||
|
server_mock.assert_called_once()
|
||||||
|
main, bck = server_mock.call_args.args
|
||||||
|
assert main.login == "user-test"
|
||||||
|
assert bck.login == "user-test"
|
||||||
|
assert main.password == "pwd-test"
|
||||||
|
assert bck.password == "pwd-test"
|
||||||
|
assert main.host == "main.host"
|
||||||
|
assert bck.host == "backup.host"
|
||||||
|
assert server_mock.call_args.kwargs["check_frequency"] == 15
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_process_requests():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", False),
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{BACKUP_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("BACKUP", "BACKUP", False),
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/traffic/interface",
|
||||||
|
body=generate_diagnostics_traffic_interface_paylaod(),
|
||||||
|
)
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.main_ha_state.state"
|
||||||
|
) as main_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.backup_ha_state.state"
|
||||||
|
) as backup_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_received.set"
|
||||||
|
) as active_server_bytes_received_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_transmitted.set"
|
||||||
|
) as active_server_bytes_transmitted_mock:
|
||||||
|
process_requests(
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
|
||||||
|
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
|
||||||
|
)
|
||||||
|
main_ha_state_mock.assert_called_once_with("active")
|
||||||
|
backup_ha_state_mock.assert_called_once_with("hot_standby")
|
||||||
|
active_server_bytes_received_mock.assert_called_once_with(11725192686820)
|
||||||
|
active_server_bytes_transmitted_mock.assert_called_once_with(2489262014203)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_process_requests_backend_active():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", True),
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{BACKUP_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", False),
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{BACKUP_HOST}/api/diagnostics/traffic/interface",
|
||||||
|
body=generate_diagnostics_traffic_interface_paylaod(),
|
||||||
|
)
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.main_ha_state.state"
|
||||||
|
) as main_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.backup_ha_state.state"
|
||||||
|
) as backup_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_received.set"
|
||||||
|
) as active_server_bytes_received_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_transmitted.set"
|
||||||
|
) as active_server_bytes_transmitted_mock:
|
||||||
|
process_requests(
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
|
||||||
|
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
|
||||||
|
)
|
||||||
|
main_ha_state_mock.assert_called_once_with("maintenancemode")
|
||||||
|
backup_ha_state_mock.assert_called_once_with("active")
|
||||||
|
active_server_bytes_received_mock.assert_called_once_with(11725192686820)
|
||||||
|
active_server_bytes_transmitted_mock.assert_called_once_with(2489262014203)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_process_no_active():
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{MAIN_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", True),
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{BACKUP_HOST}/api/diagnostics/interface/get_vip_status/",
|
||||||
|
body=generate_get_vip_status_paylaod("MASTER", "MASTER", True),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://{BACKUP_HOST}/api/diagnostics/traffic/interface",
|
||||||
|
body=generate_diagnostics_traffic_interface_paylaod(),
|
||||||
|
)
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.main_ha_state.state"
|
||||||
|
) as main_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.backup_ha_state.state"
|
||||||
|
) as backup_ha_state_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_received.set"
|
||||||
|
) as active_server_bytes_received_mock:
|
||||||
|
with mock.patch(
|
||||||
|
"opnsense_exporter.server.active_server_bytes_transmitted.set"
|
||||||
|
) as active_server_bytes_transmitted_mock:
|
||||||
|
process_requests(
|
||||||
|
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
|
||||||
|
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
|
||||||
|
)
|
||||||
|
main_ha_state_mock.assert_called_once_with("maintenancemode")
|
||||||
|
backup_ha_state_mock.assert_called_once_with("unavailable")
|
||||||
|
active_server_bytes_received_mock.assert_called_once_with(-1)
|
||||||
|
active_server_bytes_transmitted_mock.assert_called_once_with(-1)
|
Loading…
Reference in a new issue