Compare commits

..

10 commits

Author SHA1 Message Date
b59a6368c9 add Dockerfile and docker-compose.yml with environmets 2023-10-20 19:59:19 +02:00
Pierre Verkest
d56f8dba6d Allow empty string interface to not call the OPNsense traffic diagnostic REST API endpoint 2023-09-06 09:09:41 +02:00
Pierre Verkest
4fbd6c94c7 remove main_ha_state and backup_ha_state
those metrics have been replaced by opnsense_server_ha_state in v0.5.1
2023-09-04 00:32:02 +02:00
Pierre Verkest
57d2ff9485 Bump version: 0.5.0 → 0.5.1 2023-09-04 00:22:03 +02:00
Pierre Verkest
bea721a275 FIX opnsense_server_ha_state metric
I've forgot to implement the main part calling state() on metric
2023-09-04 00:21:56 +02:00
Pierre Verkest
98f832ec3b Bump version: 0.4.0 → 0.5.0 2023-09-04 00:07:36 +02:00
Pierre Verkest
76b1338ae6 Create opnsense_server_ha_state metric
in order to replace
 and  that will be removed
in version 1.0.0.

Also use Enum to store possible OPNsense HA states to avoid typo
2023-09-04 00:06:17 +02:00
Pierre Verkest
7501a0d9b8 Allow to monitor multiple interfaces 2023-09-03 23:38:34 +02:00
Pierre Verkest
334be5b4c2 refactor create OPNSensePrometheusExporter class
goals is to avoid to transmit all params over sub calls
2023-09-03 22:08:24 +02:00
Pierre Verkest
091ee429f1 Add role label in metrics
I expecte to refactor and merge metrics together so
later those labels will be required
2023-09-03 21:52:19 +02:00
11 changed files with 861 additions and 348 deletions

View file

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.4.0
current_version = 0.5.1
commit = True
tag = True

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
CHECK_FREQUENCY_SECONDS: default value for --check-frequency-seconds param
OPNSENSE_MAIN_HOST: default value for --main-host param
OPNSENSE_BACKUP_HOST: default value for --backup-host param
OPNSENSE_USERNAME: default value for --opnsense-user param
OPNSENSE_PASSWORD: default value for --opnsense-password param
OPNSENSE_INTERFACES: default value for --opnsense-interfaces param

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM python:latest
#WORKDIR /usr/app/src
# Install package
WORKDIR /code
COPY . .
#COPY prometheus-ssh-exporter.py ./
#COPY requirements.txt ./
RUN pip3 install -r requirements.txt
RUN python -u setup.py install
# Set this to the port you want to expose
EXPOSE 8000
# Set the -p option to the port you exposed above, defaults to 8000
#CMD ["python", "-u", "opnsense-exporter"]
#CMD ["sleep", "60"]
CMD ["opnsense-exporter"]

View file

@ -26,16 +26,24 @@ This exporter gives following metrics, all metrics received following labels:
- `instance`: by default this is set with the hostname where is running this exporter service
- `host`: the host of the OPNSense
- `role`: `main` or `backup` to determine the OPNSense server role.
### Enums
- `opnsense_main_ha_state`: OPNSense HA state of the MAIN server
- `opnsense_backup_ha_state`: OPNSense HA state of the BACKUP server
- `opnsense_main_ha_state`: (deprecated) OPNSense HA state of the MAIN server
- `opnsense_backup_ha_state`: (deprecated) OPNSense HA state of the BACKUP server
- `opnsense_server_ha_state`: OPNSense HA state, on of following value:
- **active**: that OPNSense server is receiving traffic
- **hot_standby**: the OPNSense server is ready to be promote as active server
- **maintenancemode**: the OPNSense server was turned into maintenance mode
- **unavailable**: the OPNSense server wasn't accessible or return unexpected value
### Gauges
- `opnsense_active_server_bytes_received`: Active OPNSense server bytes received on WAN interface
- `opnsense_active_server_bytes_transmitted`: Active OPNSense server bytes transmitted on WAN interface
- `opnsense_active_server_traffic_rate`: Active OPNSense server traffic rate per interfaces bits/s
add following labels:
- **interface**: the interface to export (values given using `--opnsense-interfaces`)
- **metric**: the metric name (as today one of `rate_bits_in`, `rate_bits_in`)
## Usage
@ -46,6 +54,7 @@ opnsense-exporter --help
usage: opnsense-exporter [-h] [--check-frequency-seconds FREQUENCY]
[--main-host MAIN] [--backup-host BACKUP]
[--opnsense-user USER]
[--opnsense-interfaces INTERFACES]
[--opnsense-password PASSWORD]
[--prometheus-instance PROM_INSTANCE]
@ -60,17 +69,24 @@ optional arguments:
MAIN OPNsense server that should be in `active`
state in normal configuration.
--backup-host BACKUP, -b BACKUP
BACKUP OPNsense server that should be
`hot_standby` state in normal configuration.
BACKUP OPNsense server that should be `hot_standby`
state in normal configuration.
--opnsense-user USER, -u USER
OPNsense user. Expect to be the same on MAIN and
BACKUP servers
--opnsense-interfaces INTERFACES, -i INTERFACES
OPNsense interfaces (coma separated) list to
export trafic rates (bytes/s). An empty string ''
means not calling the traffic diagnostic REST API
so no `opnsense_active_server_traffic_rate`
metric. (default: wan,lan)
--opnsense-password PASSWORD, -p PASSWORD
OPNsense password. Expect to be the same on MAIN
and BACKUP servers
--prometheus-instance PROM_INSTANCE
Exporter Instance name, default value computed
with hostname where the server is running. Use to
Exporter Instance name, default value computed with
hostname where the server is running. Use to set
the instance label. (default: my-opnsense-prom-exporter-server)
```
You can setup env through `.env` file or environment variables with defined as default values
@ -81,25 +97,52 @@ You can setup env through `.env` file or environment variables with defined as d
- **OPNSENSE_BACKUP_HOST**: default value for `--backup-host` param
- **OPNSENSE_USERNAME**: default value for `--opnsense-user` param
- **OPNSENSE_PASSWORD**: default value for `--opnsense-password` param
- **OPNSENSE_INTERFACES**: default value for `--opnsense-interfaces` param
## Roadmap
- allow to change the listening port (today it force using `8000`)
- allow to configure timeouts using environment variables
- improves logging to get a debug mode to understand errors based on unexpected payloads
## Changelog
### Version 0.4.0
### Version 1.0.0 (UNRELEASED)
Higher timeout while getting WAN traffic info
- remove `opnsense_main_ha_state` and `opnsense_backup_ha_state`
metrics marked as deprecated on version 0.5.0 and replace
by `opnsense_server_ha_state` and `role` label
- allow empty string interfaces to **not** call diagnostic
traffic REST API
### Version 0.3.0
Use proper method to compute WAN traffic
### Version 0.5.1 (2023-09-04)
### Version 0.2.0
- FIX `opnsense_server_ha_state` calls were not
implemented
Setup automatic release from gitlab while pushing new tag
### Version 0.5.0 (2023-09-04)
### Version 0.1.0
- add role label in metrics
- all to configure supervised interfaces using `--opnsense-interfaces`
- replace `active_server_bytes_received` and
`active_server_bytes_transmitted` by
`opnsense_active_server_traffic_rate`
- add `opnsense_server_ha_state` and mark `opnsense_main_ha_state`
and `opnsense_backup_ha_state` as deprecated.
Initial version
### Version 0.4.0 (2023-09-02)
- Higher timeout while getting WAN traffic info
### Version 0.3.0 (2023-09-02)
- Use proper method to compute WAN traffic
### Version 0.2.0 (2023-09-01)
- Setup automatic release from gitlab while pushing new tag
### Version 0.1.0 (2023-09-01)
- Initial version

20
docker-compose.yml Normal file
View file

@ -0,0 +1,20 @@
version: '3.4'
services:
opnsense-exporter:
image: opnsense-exporter
build:
context: .
dockerfile: ./Dockerfile
restart: always
container_name: opnsense-exporter
#network_mode: "host"
ports:
- 8000:8000
env_file:
- .env
logging:
driver: "json-file"
options:
max-file: "3"
max-size: 10m

View file

@ -1,4 +1,5 @@
import logging
from enum import Enum
import requests
from requests import RequestException
@ -6,16 +7,68 @@ from requests import RequestException
logger = logging.getLogger(__name__)
class OPNSenseHAState(Enum):
ACTIVE = "active"
HOT_STANDBY = "hot_standby"
UNAVAILABLE = "unavailable"
MAINTENANCE_MODE = "maintenancemode"
class OPNSenseTrafficMetric(Enum):
IN = "rate_bits_in"
OUT = "rate_bits_out"
class OPNSenseTraffic:
interface: str = None
metric: OPNSenseTrafficMetric = None
value: int = 0
def __init__(self, interface: str, metric: OPNSenseTrafficMetric, value: int = 0):
self.value = value
self.interface = interface
self.metric = metric
@property
def labels(self):
return {"metric": self.metric.value, "interface": self.interface}
def __eq__(self, opn_traffic):
"""Used by unittest to assert expected values"""
return (
self.interface == opn_traffic.interface
and self.metric == opn_traffic.metric
and self.value == opn_traffic.value
)
def __repr__(self):
return f"{self.interface} - {self.metric} = {self.value}"
class OPNSenseRole(Enum):
MAIN = "main"
BACKUP = "backup"
class OPNSenseAPI:
host: str = None
login: str = None
password: str = None
role: OPNSenseRole = None
def __init__(self, host, login, password):
def __init__(self, role, host, login, password):
self.role = role
self.host = host
self.login = login
self.password = password
@property
def labels(self):
return {
"host": self.host,
"role": self.role.value,
}
def prepare_url(self, path):
return f"https://{self.host}{path}"
@ -31,41 +84,46 @@ class OPNSenseAPI:
response.raise_for_status()
return response.json()
def get_interface_vip_status(self):
def get_interface_vip_status(self) -> OPNSenseHAState:
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"
return OPNSenseHAState.UNAVAILABLE
if data["carp"]["maintenancemode"]:
return "maintenancemode"
return OPNSenseHAState.MAINTENANCE_MODE
is_active = all([row["status"] == "MASTER" for row in data["rows"]])
if is_active:
return "active"
return OPNSenseHAState.ACTIVE
is_backup = all([row["status"] == "BACKUP" for row in data["rows"]])
if is_backup:
return "hot_standby"
return OPNSenseHAState.HOT_STANDBY
logger.warning(
"this host %s is no active nor backup received payload %s", self.host, data
)
return "unavailable"
return OPNSenseHAState.UNAVAILABLE
def get_wan_trafic(self):
def get_traffic(self, interfaces):
if not interfaces:
return []
try:
data = self.get("/api/diagnostics/traffic/top/wan", timeout=15)
data = self.get(f"/api/diagnostics/traffic/top/{interfaces}", timeout=15)
except RequestException as ex:
logger.error(
"Get diagnostics traffic on WAN interface for %s host failed with the following error %r",
"Get diagnostics traffic on %s interface(s) for %s host failed with the following error %r",
interfaces,
self.host,
ex,
)
return None, None
received = 0
transmitted = 0
for record in data["wan"]["records"]:
received += record["rate_bits_in"]
transmitted += record["rate_bits_out"]
return received, transmitted
return []
traffics = []
for interface in interfaces.split(","):
traffic_in = OPNSenseTraffic(interface, OPNSenseTrafficMetric.IN)
traffic_out = OPNSenseTraffic(interface, OPNSenseTrafficMetric.OUT)
for record in data.get(interface, {}).get("records", []):
traffic_in.value += record.get(OPNSenseTrafficMetric.IN.value, 0)
traffic_out.value += record.get(OPNSenseTrafficMetric.OUT.value, 0)
traffics.extend([traffic_in, traffic_out])
return traffics

View file

@ -1,4 +1,5 @@
import argparse
import logging
import os
import socket
import time
@ -6,84 +7,85 @@ import time
from dotenv import load_dotenv
from prometheus_client import Enum, Gauge, start_http_server
from opnsense_exporter.opnsense_api import OPNSenseAPI
from opnsense_exporter.opnsense_api import OPNSenseAPI, OPNSenseHAState, OPNSenseRole
logger = logging.getLogger(__name__)
load_dotenv()
HA_STATES = ["active", "hot_standby", "unavailable", "maintenancemode"]
main_ha_state = Enum(
"opnsense_main_ha_state",
"OPNSense HA state of the MAIN server",
HA_STATES = [enum.value for enum in list(OPNSenseHAState)]
opnsense_server_ha_state = Enum(
"opnsense_server_ha_state",
"OPNSense server HA state",
[
"instance",
"host",
"role",
],
states=HA_STATES,
)
backup_ha_state = Enum(
"opnsense_backup_ha_state",
"OPNSense HA state of the BACKUP server",
[
"instance",
"host",
],
states=HA_STATES,
)
active_server_bytes_received = Gauge(
"opnsense_active_server_bytes_received",
"Active OPNSense server bytes received on WAN interface",
[
"instance",
"host",
],
)
active_server_bytes_transmitted = Gauge(
"opnsense_active_server_bytes_transmitted",
"Active OPNSense server bytes transmitted on WAN interface",
opnsense_active_server_traffic_rate = Gauge(
"opnsense_active_server_traffic_rate",
"Active OPNSense server bytes in/out per interface",
[
"instance",
"host",
"role",
"interface",
"metric",
],
)
def process_requests(main, backup, exporter_instance: str = ""):
"""A dummy function that takes some time."""
main_state = main.get_interface_vip_status()
backup_sate = backup.get_interface_vip_status()
main_ha_state.labels(instance=exporter_instance, host=main.host).state(main_state)
backup_ha_state.labels(instance=exporter_instance, host=backup.host).state(
backup_sate
)
active_opnsense = None
if main_state == "active":
active_opnsense = main
if backup_sate == "active":
active_opnsense = backup
if active_opnsense:
bytes_received, bytes_transmitted = active_opnsense.get_wan_trafic()
if bytes_received or bytes_received == 0:
active_server_bytes_received.labels(
instance=exporter_instance, host=active_opnsense.host
).set(bytes_received)
if bytes_transmitted or bytes_transmitted == 0:
active_server_bytes_transmitted.labels(
instance=exporter_instance, host=active_opnsense.host
).set(bytes_transmitted)
def start_server(
class OPNSensePrometheusExporter:
def __init__(
self,
main: OPNSenseAPI,
backup: OPNSenseAPI,
check_frequency: int = 1,
interfaces,
exporter_instance: str = "",
):
check_frequency: int = 1,
):
self.main = main
self.backup = backup
self.interfaces = interfaces
self.exporter_instance = exporter_instance
self.check_frequency = check_frequency
def process_requests(self):
"""A dummy function that takes some time."""
main_state = self.main.get_interface_vip_status()
backup_sate = self.backup.get_interface_vip_status()
opnsense_server_ha_state.labels(
instance=self.exporter_instance, **self.main.labels
).state(main_state.value)
opnsense_server_ha_state.labels(
instance=self.exporter_instance, **self.backup.labels
).state(backup_sate.value)
active_opnsense = None
if main_state == OPNSenseHAState.ACTIVE:
active_opnsense = self.main
if backup_sate == OPNSenseHAState.ACTIVE:
active_opnsense = self.backup
if active_opnsense:
for traffic in active_opnsense.get_traffic(self.interfaces):
if traffic.value:
opnsense_active_server_traffic_rate.labels(
instance=self.exporter_instance,
**active_opnsense.labels,
**traffic.labels
).set(traffic.value)
def start_server(self, port=8000):
# Start up the server to expose the metrics.
start_http_server(8000)
start_http_server(port)
logger.info("listen port %s", port)
# Generate some requests.
while True:
process_requests(main, backup, exporter_instance=exporter_instance)
time.sleep(check_frequency)
self.process_requests()
time.sleep(self.check_frequency)
def run():
@ -123,6 +125,16 @@ def run():
default=os.environ.get("OPNSENSE_USERNAME", None),
help="OPNsense user. Expect to be the same on MAIN and BACKUP servers",
)
parser.add_argument(
"--opnsense-interfaces",
"-i",
type=str,
dest="interfaces",
default=os.environ.get("OPNSENSE_INTERFACES", "wan,lan"),
help="OPNsense interfaces (coma separated) list to export trafic rates (bytes/s). "
"An empty string '' means not calling the traffic diagnostic REST API so no "
"`opnsense_active_server_traffic_rate` metric.",
)
parser.add_argument(
"--opnsense-password",
"-p",
@ -143,9 +155,19 @@ def run():
)
arguments = parser.parse_args()
start_server(
OPNSenseAPI(arguments.main, arguments.user, arguments.password),
OPNSenseAPI(arguments.backup, arguments.user, arguments.password),
server = OPNSensePrometheusExporter(
OPNSenseAPI(
OPNSenseRole.MAIN, arguments.main, arguments.user, arguments.password
),
OPNSenseAPI(
OPNSenseRole.BACKUP, arguments.backup, arguments.user, arguments.password
),
arguments.interfaces,
check_frequency=arguments.frequency,
exporter_instance=arguments.prom_instance,
)
server.start_server()
# return the server instance mainly for test purpose
return server

View file

@ -3,7 +3,7 @@ from urllib.parse import urlparse
from setuptools import find_packages, setup
version = "0.4.0"
version = "0.5.1"
HERE = pathlib.Path(__file__).parent

View file

@ -47,126 +47,131 @@ def generate_get_vip_status_paylaod(state_wan, state_lan, maintenance_mode):
def generate_diagnostics_traffic_interface_paylaod():
# wan - rate_bits_in: 101026
# wan - rate_bits_out: 86020
# lan - rate_bits_in: 188490
# lan - rate_bits_out: 952
return json.dumps(
{
"wan": {
"records": [
{
"address": "0.1.2.3",
"rate_bits_in": 15300,
"rate_bits_out": 1720,
"rate_bits": 17020,
"cumulative_bytes_in": 3830,
"cumulative_bytes_out": 441,
"cumulative_bytes": 4271,
"rate_bits_in": 62300,
"rate_bits_out": 66100,
"rate_bits": 128400,
"cumulative_bytes_in": 15600,
"cumulative_bytes_out": 16500,
"cumulative_bytes": 32100,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "15.3Kb",
"rate_bits": 15300,
"cumulative": "3.83KB",
"cumulative_bytes": 3830,
"rate": "62.3Kb",
"rate_bits": 62300,
"cumulative": "15.6KB",
"cumulative_bytes": 15600,
"tags": ["local"],
}
],
"rname": "fake value",
"rate_in": "15.3 kb",
"rate_out": "1.72 kb",
"rate": "17.02 kb",
"cumulative_in": "3.83 kb",
"cumulative_out": "441.0 b",
"cumulative": "4.27 kb",
"rname": "fake rname value",
"rate_in": "62.3 kb",
"rate_out": "66.1 kb",
"rate": "128.4 kb",
"cumulative_in": "15.6 kb",
"cumulative_out": "16.5 kb",
"cumulative": "32.1 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 4470,
"rate_bits_out": 7290,
"rate_bits": 11760,
"cumulative_bytes_in": 1120,
"cumulative_bytes_out": 1820,
"cumulative_bytes": 2940,
"rate_bits_in": 36200,
"rate_bits_out": 16100,
"rate_bits": 52300,
"cumulative_bytes_in": 9060,
"cumulative_bytes_out": 4020,
"cumulative_bytes": 13080,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "4.47Kb",
"rate_bits": 4470,
"cumulative": "1.12KB",
"cumulative_bytes": 1120,
"rate": "36.2Kb",
"rate_bits": 36200,
"cumulative": "9.06KB",
"cumulative_bytes": 9060,
"tags": ["local"],
}
],
"rname": "fake value",
"rate_in": "4.47 kb",
"rate_out": "7.29 kb",
"rate": "11.76 kb",
"cumulative_in": "1.12 kb",
"cumulative_out": "1.82 kb",
"cumulative": "2.94 kb",
"rname": "fake rname value",
"rate_in": "36.2 kb",
"rate_out": "16.1 kb",
"rate": "52.3 kb",
"cumulative_in": "9.06 kb",
"cumulative_out": "4.02 kb",
"cumulative": "13.08 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 272,
"rate_bits_out": 272,
"rate_bits": 544,
"cumulative_bytes_in": 68,
"cumulative_bytes_out": 68,
"cumulative_bytes": 136,
"rate_bits_in": 1790,
"rate_bits_out": 1520,
"rate_bits": 3310,
"cumulative_bytes_in": 459,
"cumulative_bytes_out": 389,
"cumulative_bytes": 848,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "272b",
"rate_bits": 272,
"cumulative": "68B",
"cumulative_bytes": 68,
"rate": "1.79Kb",
"rate_bits": 1790,
"cumulative": "459B",
"cumulative_bytes": 459,
"tags": ["local"],
}
],
"rname": "fake value",
"rate_in": "272.0 b",
"rate_out": "272.0 b",
"rate": "544.0 b",
"cumulative_in": "68.0 b",
"cumulative_out": "68.0 b",
"cumulative": "136.0 b",
"rname": "fake rname value",
"rate_in": "1.79 kb",
"rate_out": "1.52 kb",
"rate": "3.31 kb",
"cumulative_in": "459.0 b",
"cumulative_out": "389.0 b",
"cumulative": "848.0 b",
},
{
"address": "0.1.2.3",
"rate_bits_in": 272,
"rate_bits_out": 272,
"rate_bits": 544,
"cumulative_bytes_in": 68,
"cumulative_bytes_out": 68,
"cumulative_bytes": 136,
"rate_bits_in": 512,
"rate_bits_out": 1580,
"rate_bits": 2092,
"cumulative_bytes_in": 128,
"cumulative_bytes_out": 405,
"cumulative_bytes": 533,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "272b",
"rate_bits": 272,
"cumulative": "68B",
"cumulative_bytes": 68,
"rate": "512b",
"rate_bits": 512,
"cumulative": "128B",
"cumulative_bytes": 128,
"tags": ["local"],
}
],
"rname": "fake value",
"rate_in": "272.0 b",
"rate_out": "272.0 b",
"rate": "544.0 b",
"cumulative_in": "68.0 b",
"cumulative_out": "68.0 b",
"cumulative": "136.0 b",
"rname": "fake rname value",
"rate_in": "512.0 b",
"rate_out": "1.58 kb",
"rate": "2.09 kb",
"cumulative_in": "128.0 b",
"cumulative_out": "405.0 b",
"cumulative": "533.0 b",
},
{
"address": "0.1.2.3",
"rate_bits_in": 0,
"rate_bits_out": 480,
"rate_bits": 480,
"rate_bits_out": 448,
"rate_bits": 448,
"cumulative_bytes_in": 0,
"cumulative_bytes_out": 120,
"cumulative_bytes": 120,
"cumulative_bytes_out": 112,
"cumulative_bytes": 112,
"tags": [],
"details": [
{
@ -176,7 +181,26 @@ def generate_diagnostics_traffic_interface_paylaod():
"cumulative": "0B",
"cumulative_bytes": 0,
"tags": ["local"],
}
],
"rname": "fake rname value",
"rate_in": "0.0 b",
"rate_out": "448.0 b",
"rate": "448.0 b",
"cumulative_in": "0.0 b",
"cumulative_out": "112.0 b",
"cumulative": "112.0 b",
},
{
"address": "0.1.2.3",
"rate_bits_in": 0,
"rate_bits_out": 272,
"rate_bits": 272,
"cumulative_bytes_in": 0,
"cumulative_bytes_out": 68,
"cumulative_bytes": 68,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "0b",
@ -184,15 +208,15 @@ def generate_diagnostics_traffic_interface_paylaod():
"cumulative": "0B",
"cumulative_bytes": 0,
"tags": ["local"],
},
}
],
"rname": "fake value",
"rname": "fake rname value",
"rate_in": "0.0 b",
"rate_out": "480.0 b",
"rate": "480.0 b",
"rate_out": "272.0 b",
"rate": "272.0 b",
"cumulative_in": "0.0 b",
"cumulative_out": "120.0 b",
"cumulative": "120.0 b",
"cumulative_out": "68.0 b",
"cumulative": "68.0 b",
},
{
"address": "0.1.2.3",
@ -213,7 +237,7 @@ def generate_diagnostics_traffic_interface_paylaod():
"tags": ["local"],
}
],
"rname": "fake value",
"rname": "fake rname value",
"rate_in": "224.0 b",
"rate_out": "0.0 b",
"rate": "224.0 b",
@ -223,6 +247,224 @@ def generate_diagnostics_traffic_interface_paylaod():
},
],
"status": "ok",
},
"lan": {
"records": [
{
"address": "0.1.2.3",
"rate_bits_in": 65200,
"rate_bits_out": 0,
"rate_bits": 65200,
"cumulative_bytes_in": 16270,
"cumulative_bytes_out": 0,
"cumulative_bytes": 16270,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "45.3Kb",
"rate_bits": 45300,
"cumulative": "11.3KB",
"cumulative_bytes": 11300,
"tags": ["private"],
},
{
"address": "0.1.2.3",
"rate": "19.9Kb",
"rate_bits": 19900,
"cumulative": "4.97KB",
"cumulative_bytes": 4970,
"tags": ["private"],
},
],
"rname": "fake rname value",
"rate_in": "65.2 kb",
"rate_out": "0.0 b",
"rate": "65.2 kb",
"cumulative_in": "16.27 kb",
"cumulative_out": "0.0 b",
"cumulative": "16.27 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 47900,
"rate_bits_out": 0,
"rate_bits": 47900,
"cumulative_bytes_in": 12000,
"cumulative_bytes_out": 0,
"cumulative_bytes": 12000,
"tags": ["private"],
"details": [
{
"address": "0.1.2.3",
"rate": "47.9Kb",
"rate_bits": 47900,
"cumulative": "12.0KB",
"cumulative_bytes": 12000,
"tags": [],
}
],
"rname": "fake rname value",
"rate_in": "47.9 kb",
"rate_out": "0.0 b",
"rate": "47.9 kb",
"cumulative_in": "12.0 kb",
"cumulative_out": "0.0 b",
"cumulative": "12.0 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 36200,
"rate_bits_out": 0,
"rate_bits": 36200,
"cumulative_bytes_in": 9060,
"cumulative_bytes_out": 0,
"cumulative_bytes": 9060,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "36.2Kb",
"rate_bits": 36200,
"cumulative": "9.06KB",
"cumulative_bytes": 9060,
"tags": ["private"],
}
],
"rname": "fake rname value",
"rate_in": "36.2 kb",
"rate_out": "0.0 b",
"rate": "36.2 kb",
"cumulative_in": "9.06 kb",
"cumulative_out": "0.0 b",
"cumulative": "9.06 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 19200,
"rate_bits_out": 0,
"rate_bits": 19200,
"cumulative_bytes_in": 4814,
"cumulative_bytes_out": 0,
"cumulative_bytes": 4814,
"tags": ["private"],
"details": [
{
"address": "0.1.2.3",
"rate": "16.1Kb",
"rate_bits": 16100,
"cumulative": "4.02KB",
"cumulative_bytes": 4020,
"tags": [],
},
{
"address": "0.1.2.3",
"rate": "1.58Kb",
"rate_bits": 1580,
"cumulative": "405B",
"cumulative_bytes": 405,
"tags": [],
},
{
"address": "0.1.2.3",
"rate": "1.52Kb",
"rate_bits": 1520,
"cumulative": "389B",
"cumulative_bytes": 389,
"tags": [],
},
],
"rname": "fake rname value",
"rate_in": "19.2 kb",
"rate_out": "0.0 b",
"rate": "19.2 kb",
"cumulative_in": "4.81 kb",
"cumulative_out": "0.0 b",
"cumulative": "4.81 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 18200,
"rate_bits_out": 0,
"rate_bits": 18200,
"cumulative_bytes_in": 4550,
"cumulative_bytes_out": 0,
"cumulative_bytes": 4550,
"tags": ["private"],
"details": [
{
"address": "0.1.2.3",
"rate": "18.2Kb",
"rate_bits": 18200,
"cumulative": "4.55KB",
"cumulative_bytes": 4550,
"tags": [],
}
],
"rname": "fake rname value",
"rate_in": "18.2 kb",
"rate_out": "0.0 b",
"rate": "18.2 kb",
"cumulative_in": "4.55 kb",
"cumulative_out": "0.0 b",
"cumulative": "4.55 kb",
},
{
"address": "0.1.2.3",
"rate_bits_in": 1790,
"rate_bits_out": 0,
"rate_bits": 1790,
"cumulative_bytes_in": 459,
"cumulative_bytes_out": 0,
"cumulative_bytes": 459,
"tags": [],
"details": [
{
"address": "0.1.2.3",
"rate": "1.79Kb",
"rate_bits": 1790,
"cumulative": "459B",
"cumulative_bytes": 459,
"tags": ["private"],
}
],
"rname": "fake rname value",
"rate_in": "1.79 kb",
"rate_out": "0.0 b",
"rate": "1.79 kb",
"cumulative_in": "459.0 b",
"cumulative_out": "0.0 b",
"cumulative": "459.0 b",
},
{
"address": "0.1.2.3",
"rate_bits_in": 0,
"rate_bits_out": 952,
"rate_bits": 952,
"cumulative_bytes_in": 0,
"cumulative_bytes_out": 238,
"cumulative_bytes": 238,
"tags": ["private"],
"details": [
{
"address": "0.1.2.3",
"rate": "0b",
"rate_bits": 0,
"cumulative": "0B",
"cumulative_bytes": 0,
"tags": ["private"],
}
],
"rname": "fake rname value",
"rate_in": "0.0 b",
"rate_out": "952.0 b",
"rate": "952.0 b",
"cumulative_in": "0.0 b",
"cumulative_out": "238.0 b",
"cumulative": "238.0 b",
},
],
"status": "ok",
},
}
)

View file

@ -1,8 +1,14 @@
import responses
from opnsense_exporter.opnsense_api import OPNSenseAPI
from opnsense_exporter.opnsense_api import (
OPNSenseAPI,
OPNSenseRole,
OPNSenseTraffic,
OPNSenseTrafficMetric,
)
from .common import (
BACKUP_HOST,
LOGIN,
MAIN_HOST,
PASSWORD,
@ -20,7 +26,10 @@ def test_get_interface_vip_status_active():
)
assert (
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status() == "active"
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD)
.get_interface_vip_status()
.value
== "active"
)
@ -33,7 +42,9 @@ def test_get_interface_vip_status_backup():
)
assert (
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD)
.get_interface_vip_status()
.value
== "hot_standby"
)
@ -47,7 +58,9 @@ def test_get_interface_vip_status_mainteance_mode():
)
assert (
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD)
.get_interface_vip_status()
.value
== "maintenancemode"
)
@ -60,7 +73,9 @@ def test_get_interface_vip_status_unavailable_weird_case():
body=generate_get_vip_status_paylaod("MASTER", "BACKUP", False),
)
assert (
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD)
.get_interface_vip_status()
.value
== "unavailable"
)
@ -74,33 +89,65 @@ def test_get_interface_vip_status_unavailable_rest_api_error():
status=404,
)
assert (
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_interface_vip_status()
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD)
.get_interface_vip_status()
.value
== "unavailable"
)
@responses.activate
def test_get_wan_traffic():
def test_get_traffic():
responses.add(
responses.GET,
f"https://{MAIN_HOST}/api/diagnostics/traffic/top/wan",
f"https://{MAIN_HOST}/api/diagnostics/traffic/top/wan,lan",
body=generate_diagnostics_traffic_interface_paylaod(),
)
assert OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_wan_trafic() == (
20538,
10034,
assert OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD).get_traffic(
"wan,lan"
) == [
OPNSenseTraffic("wan", OPNSenseTrafficMetric.IN, value=101026),
OPNSenseTraffic("wan", OPNSenseTrafficMetric.OUT, value=86020),
OPNSenseTraffic("lan", OPNSenseTrafficMetric.IN, value=188490),
OPNSenseTraffic("lan", OPNSenseTrafficMetric.OUT, value=952),
]
@responses.activate
def test_get_traffic_none():
responses.add(
responses.GET,
f"https://{MAIN_HOST}/api/diagnostics/traffic/top/test-not-found",
json={"error": "not found"},
status=404,
)
assert (
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD).get_traffic(
"test-not-found"
)
== []
)
@responses.activate
def test_get_wan_traffic_none():
responses.add(
def test_get_traffic_empty_string():
rsp = responses.add(
responses.GET,
f"https://{MAIN_HOST}/api/diagnostics/traffic/top/wan",
json={"error": "not found"},
status=404,
f"https://{MAIN_HOST}/api/diagnostics/traffic/top/",
json={"not": "called"},
)
assert OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD).get_wan_trafic() == (
None,
None,
assert (
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD).get_traffic("") == []
)
assert rsp.call_count == 0
def test_labels():
assert OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD).labels == {
"role": "main",
"host": MAIN_HOST,
}
assert OPNSenseAPI(OPNSenseRole.BACKUP, BACKUP_HOST, LOGIN, PASSWORD).labels == {
"role": "backup",
"host": BACKUP_HOST,
}

View file

@ -1,9 +1,10 @@
from typing import List
from unittest import mock
import responses
from opnsense_exporter.opnsense_api import OPNSenseAPI
from opnsense_exporter.server import process_requests, run
from opnsense_exporter.opnsense_api import OPNSenseAPI, OPNSenseRole
from opnsense_exporter.server import OPNSensePrometheusExporter, run
from .common import (
BACKUP_HOST,
@ -17,31 +18,57 @@ from .common import (
class FakePromMetric:
_labels = {}
_labels_calls = None
def __init__(self):
self._labels = {}
self._labels_calls = []
@property
def count_labels_calls(self) -> int:
return len(self._labels_calls)
def labels(self, *args, **kwargs):
self._labels = kwargs
self._labels_calls.append(kwargs)
return self
class FakePromEnum(FakePromMetric):
_state = None
count_state_calls = 0
_state: str = None
_state_calls: List[str] = []
def state(self, state):
self.count_state_calls += 1
def __init__(self):
super().__init__()
self._state_calls = []
@property
def count_state_calls(self) -> int:
return len(self._state_calls)
def state(self, state: str):
self._state = state
self._state_calls.append(state)
class FakePromGauge(FakePromMetric):
value = None
count_set_calls = 0
_value: int = None
_set_calls: List[int] = []
def set(self, value):
self.count_set_calls += 1
self.value = value
def __init__(self):
super().__init__()
self._set_calls = []
@property
def count_set_calls(self) -> int:
return len(self._set_calls)
def set(self, value: int):
self._value = value
self._set_calls.append(value)
@mock.patch("opnsense_exporter.server.start_server")
@mock.patch("opnsense_exporter.server.OPNSensePrometheusExporter.start_server")
def test_parser(server_mock):
with mock.patch(
"sys.argv",
@ -57,20 +84,25 @@ def test_parser(server_mock):
"user-test",
"-p",
"pwd-test",
"-i",
"efg,hij",
"--prometheus-instance",
"server-hostname-instance",
],
):
run()
server = 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
assert server.main.role == OPNSenseRole.MAIN
assert server.main.host == "main.host"
assert server.main.login == "user-test"
assert server.main.password == "pwd-test"
assert server.backup.role == OPNSenseRole.BACKUP
assert server.backup.host == "backup.host"
assert server.backup.login == "user-test"
assert server.backup.password == "pwd-test"
assert server.check_frequency == 15
assert server.interfaces == "efg,hij"
@responses.activate
@ -91,49 +123,56 @@ def test_process_requests():
body=generate_diagnostics_traffic_interface_paylaod(),
)
main_ha_state_mock = FakePromEnum()
backup_ha_state_mock = FakePromEnum()
active_server_bytes_received_mock = FakePromGauge()
active_server_bytes_transmitted_mock = FakePromGauge()
opnsense_server_ha_state_mock = FakePromEnum()
opnsense_active_server_traffic_rate_mock = FakePromGauge()
with mock.patch("opnsense_exporter.server.main_ha_state", new=main_ha_state_mock):
with mock.patch(
"opnsense_exporter.server.backup_ha_state", new=backup_ha_state_mock
"opnsense_exporter.server.opnsense_server_ha_state",
new=opnsense_server_ha_state_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_received",
new=active_server_bytes_received_mock,
"opnsense_exporter.server.opnsense_active_server_traffic_rate",
new=opnsense_active_server_traffic_rate_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_transmitted",
new=active_server_bytes_transmitted_mock,
):
process_requests(
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
)
OPNSensePrometheusExporter(
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(OPNSenseRole.BACKUP, BACKUP_HOST, LOGIN, PASSWORD),
"wan",
).process_requests()
assert main_ha_state_mock._state == "active"
assert main_ha_state_mock.count_state_calls == 1
assert main_ha_state_mock._labels == {"instance": "", "host": MAIN_HOST}
assert backup_ha_state_mock._state == "hot_standby"
assert backup_ha_state_mock.count_state_calls == 1
assert backup_ha_state_mock._labels == {"instance": "", "host": BACKUP_HOST}
assert active_server_bytes_received_mock.value == 20538
assert active_server_bytes_received_mock.count_set_calls == 1
assert active_server_bytes_received_mock._labels == {
assert opnsense_server_ha_state_mock.count_state_calls == 2
assert opnsense_server_ha_state_mock._labels_calls == [
{
"instance": "",
"host": MAIN_HOST,
}
"role": "main",
},
{
"instance": "",
"host": BACKUP_HOST,
"role": "backup",
},
]
assert opnsense_server_ha_state_mock._state_calls == ["active", "hot_standby"]
assert active_server_bytes_transmitted_mock.value == 10034
assert active_server_bytes_transmitted_mock.count_set_calls == 1
assert active_server_bytes_transmitted_mock._labels == {
assert opnsense_active_server_traffic_rate_mock.count_set_calls == 2
assert opnsense_active_server_traffic_rate_mock._labels_calls == [
{
"instance": "",
"host": MAIN_HOST,
}
"role": "main",
"interface": "wan",
"metric": "rate_bits_in",
},
{
"instance": "",
"host": MAIN_HOST,
"role": "main",
"interface": "wan",
"metric": "rate_bits_out",
},
]
assert opnsense_active_server_traffic_rate_mock._set_calls == [101026, 86020]
@responses.activate
@ -154,48 +193,56 @@ def test_process_requests_backup_active():
body=generate_diagnostics_traffic_interface_paylaod(),
)
main_ha_state_mock = FakePromEnum()
backup_ha_state_mock = FakePromEnum()
active_server_bytes_received_mock = FakePromGauge()
active_server_bytes_transmitted_mock = FakePromGauge()
opnsense_server_ha_state_mock = FakePromEnum()
opnsense_active_server_traffic_rate_mock = FakePromGauge()
with mock.patch("opnsense_exporter.server.main_ha_state", new=main_ha_state_mock):
with mock.patch(
"opnsense_exporter.server.backup_ha_state", new=backup_ha_state_mock
"opnsense_exporter.server.opnsense_server_ha_state",
new=opnsense_server_ha_state_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_received",
new=active_server_bytes_received_mock,
"opnsense_exporter.server.opnsense_active_server_traffic_rate",
new=opnsense_active_server_traffic_rate_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_transmitted",
new=active_server_bytes_transmitted_mock,
):
process_requests(
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
)
assert main_ha_state_mock._state == "maintenancemode"
assert main_ha_state_mock.count_state_calls == 1
assert main_ha_state_mock._labels == {"instance": "", "host": MAIN_HOST}
OPNSensePrometheusExporter(
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(OPNSenseRole.BACKUP, BACKUP_HOST, LOGIN, PASSWORD),
"wan",
).process_requests()
assert backup_ha_state_mock._state == "active"
assert backup_ha_state_mock.count_state_calls == 1
assert backup_ha_state_mock._labels == {"instance": "", "host": BACKUP_HOST}
assert active_server_bytes_received_mock.value == 20538
assert active_server_bytes_received_mock.count_set_calls == 1
assert active_server_bytes_received_mock._labels == {
assert opnsense_server_ha_state_mock.count_state_calls == 2
assert opnsense_server_ha_state_mock._labels_calls == [
{
"instance": "",
"host": MAIN_HOST,
"role": "main",
},
{
"instance": "",
"host": BACKUP_HOST,
}
"role": "backup",
},
]
assert opnsense_server_ha_state_mock._state_calls == ["maintenancemode", "active"]
assert active_server_bytes_transmitted_mock.value == 10034
assert active_server_bytes_transmitted_mock.count_set_calls == 1
assert active_server_bytes_transmitted_mock._labels == {
assert opnsense_active_server_traffic_rate_mock.count_set_calls == 2
opnsense_active_server_traffic_rate_mock._labels_calls == [
{
"instance": "",
"host": BACKUP_HOST,
}
"role": "backup",
"interface": "wan",
"metric": "rate_bits_in",
},
{
"instance": "",
"host": BACKUP_HOST,
"role": "backup",
"interface": "wan",
"metric": "rate_bits_out",
},
]
assert opnsense_active_server_traffic_rate_mock._set_calls == [101026, 86020]
@responses.activate
@ -217,38 +264,42 @@ def test_process_no_active():
body=generate_diagnostics_traffic_interface_paylaod(),
)
main_ha_state_mock = FakePromEnum()
backup_ha_state_mock = FakePromEnum()
active_server_bytes_received_mock = FakePromGauge()
active_server_bytes_transmitted_mock = FakePromGauge()
opnsense_server_ha_state_mock = FakePromEnum()
opnsense_active_server_traffic_rate_mock = FakePromGauge()
with mock.patch("opnsense_exporter.server.main_ha_state", new=main_ha_state_mock):
with mock.patch(
"opnsense_exporter.server.backup_ha_state", new=backup_ha_state_mock
"opnsense_exporter.server.opnsense_server_ha_state",
new=opnsense_server_ha_state_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_received",
new=active_server_bytes_received_mock,
"opnsense_exporter.server.opnsense_active_server_traffic_rate",
new=opnsense_active_server_traffic_rate_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_transmitted",
new=active_server_bytes_transmitted_mock,
):
process_requests(
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
)
OPNSensePrometheusExporter(
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(OPNSenseRole.BACKUP, BACKUP_HOST, LOGIN, PASSWORD),
"wan",
).process_requests()
assert main_ha_state_mock._state == "maintenancemode"
assert main_ha_state_mock.count_state_calls == 1
assert main_ha_state_mock._labels == {"instance": "", "host": MAIN_HOST}
assert opnsense_server_ha_state_mock.count_state_calls == 2
assert opnsense_server_ha_state_mock._labels_calls == [
{
"instance": "",
"host": MAIN_HOST,
"role": "main",
},
{
"instance": "",
"host": BACKUP_HOST,
"role": "backup",
},
]
assert opnsense_server_ha_state_mock._state_calls == [
"maintenancemode",
"unavailable",
]
assert backup_ha_state_mock._state == "unavailable"
assert backup_ha_state_mock.count_state_calls == 1
assert backup_ha_state_mock._labels == {"instance": "", "host": BACKUP_HOST}
assert active_server_bytes_received_mock.count_set_calls == 0
assert active_server_bytes_transmitted_mock.count_set_calls == 0
assert opnsense_active_server_traffic_rate_mock.count_set_calls == 0
@responses.activate
@ -270,34 +321,36 @@ def test_process_with_falsy_value():
status=404,
)
main_ha_state_mock = FakePromEnum()
backup_ha_state_mock = FakePromEnum()
active_server_bytes_received_mock = FakePromGauge()
active_server_bytes_transmitted_mock = FakePromGauge()
opnsense_server_ha_state_mock = FakePromEnum()
opnsense_active_server_traffic_rate_mock = FakePromGauge()
with mock.patch("opnsense_exporter.server.main_ha_state", new=main_ha_state_mock):
with mock.patch(
"opnsense_exporter.server.backup_ha_state", new=backup_ha_state_mock
"opnsense_exporter.server.opnsense_server_ha_state",
new=opnsense_server_ha_state_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_received",
new=active_server_bytes_received_mock,
"opnsense_exporter.server.opnsense_active_server_traffic_rate",
new=opnsense_active_server_traffic_rate_mock,
):
with mock.patch(
"opnsense_exporter.server.active_server_bytes_transmitted",
new=active_server_bytes_transmitted_mock,
):
process_requests(
OPNSenseAPI(MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(BACKUP_HOST, LOGIN, PASSWORD),
)
assert main_ha_state_mock._state == "active"
assert main_ha_state_mock.count_state_calls == 1
assert main_ha_state_mock._labels == {"instance": "", "host": MAIN_HOST}
OPNSensePrometheusExporter(
OPNSenseAPI(OPNSenseRole.MAIN, MAIN_HOST, LOGIN, PASSWORD),
OPNSenseAPI(OPNSenseRole.BACKUP, BACKUP_HOST, LOGIN, PASSWORD),
"wan",
).process_requests()
assert backup_ha_state_mock.count_state_calls == 1
assert backup_ha_state_mock._state == "hot_standby"
assert backup_ha_state_mock._labels == {"instance": "", "host": BACKUP_HOST}
assert opnsense_server_ha_state_mock.count_state_calls == 2
assert opnsense_server_ha_state_mock._labels_calls == [
{
"instance": "",
"host": MAIN_HOST,
"role": "main",
},
{
"instance": "",
"host": BACKUP_HOST,
"role": "backup",
},
]
assert opnsense_server_ha_state_mock._state_calls == ["active", "hot_standby"]
assert active_server_bytes_received_mock.count_set_calls == 0
assert active_server_bytes_transmitted_mock.count_set_calls == 0
assert opnsense_active_server_traffic_rate_mock.count_set_calls == 0