Skip to content

Commit 96a6e50

Browse files
epicgdogevanugarte
andauthored
JSON from args + merged to one container (#129)
* ips parsed from config.json * added logs for debugging * absolute path for config * mounted config as volume * turned config path as required command argument * combined printer & collector dockerfiles (attempt #1) * changed collector path * merged into one container merged with dev rebased to dev * cleaned up, changed parsing method, removed metrics, and moved thread merged * made config-json not required * add args to test_server * typo * more typo fixes * os.path.exists * RUN apt-get install -y software-properties-common && add-apt-repository ppa:deadsnakes/ppa && apt-get update && apt-get install -y python3.11 python3.11-venv python3.11-dev jq ssh # Create the virtual environment with Python 3.11 RUN python3.11 -m venv /opt/venv * changed requirements for newer snmp * Reverted dockerfile * typos in dockerfile * more typos * config parsing bug * isinstance fix * first implementation of pysnmp 4.x -> 7.x * changed docker image to support python 3.10 * Update printer/server.py Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> --------- Co-authored-by: evan <evanuxd@gmail.com> Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com>
1 parent 6c75252 commit 96a6e50

10 files changed

Lines changed: 159 additions & 188 deletions

File tree

collector/Dockerfile

Lines changed: 0 additions & 13 deletions
This file was deleted.

collector/requirements.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

collector/server.py

Lines changed: 0 additions & 152 deletions
This file was deleted.

docker-compose.dev.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,4 @@ services:
1717
- --development
1818
- --port=14000
1919
- --dont-delete-pdfs
20-
snmp-collector:
21-
build:
22-
context: .
23-
dockerfile: ./collector/Dockerfile
24-
ports:
25-
- 5000:5000
26-
20+
- --config-json-path=/app/config/config.json

docker-compose.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ services:
1313
- ~/.ssh/known_hosts:/app/known_hosts
1414
- "/etc/cups/ppd/:/etc/cups/ppd"
1515
tty: true
16-
snmp-collector:
17-
build:
18-
context: .
19-
dockerfile: ./collector/Dockerfile
16+
2017
# we attach the print container to an external docker
2118
# network called "poweredge". we do this so a prometheus
2219
# container can pull metrics from the server over HTTP

printer/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
# Base image from https://github.com/DrPsychick/docker-cups-airprint
22
# Docker images are here https://hub.docker.com/r/drpsychick/airprint-bridge/tags
3-
FROM drpsychick/airprint-bridge:latest
3+
FROM drpsychick/airprint-bridge:jammy
44

55
WORKDIR /app
6-
76
RUN apt-get update
87

98
RUN apt install -y python3 python3-pip python3-venv jq ssh
109

11-
# Create a virtual environment
10+
# Create the virtual environment with Python
1211
RUN python3 -m venv /opt/venv
1312

1413
# Set the virtual environment as the default Python environment
1514
ENV PATH="/opt/venv/bin:$PATH"
1615

17-
COPY ./printer/requirements.txt /app/printer/requirements.txt
16+
COPY ./printer/requirements.txt /app/printer/requirements.txt
1817

1918
RUN /opt/venv/bin/pip install -r /app/printer/requirements.txt
2019

@@ -29,3 +28,4 @@ EXPOSE 9000
2928
# The below command runs the bash script that sets up the connection to the
3029
# printers and runs PrintHandler.js
3130
ENTRYPOINT [ "./printer/what.sh" ]
31+

printer/collector.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import time
2+
import enum
3+
import logging
4+
import json
5+
import asyncio
6+
7+
from pysnmp.hlapi import *
8+
9+
from metrics import MetricsHandler
10+
11+
metrics_handler = MetricsHandler.instance()
12+
13+
14+
logging.basicConfig(
15+
# in mondo we trust
16+
format="%(asctime)s.%(msecs)03dZ %(levelname)s:%(name)s:%(message)s",
17+
datefmt="%Y-%m-%dT%H:%M:%S",
18+
level=logging.INFO,
19+
)
20+
21+
22+
class SnmpOid(enum.Enum):
23+
INK_LEVEL = ("ink_level", "1.3.6.1.2.1.43.11.1.1.9.1.1")
24+
INK_CAPACITY = ("ink_capacity", "1.3.6.1.2.1.43.11.1.1.8.1.1")
25+
PAGE_COUNT = ("page_count", "1.3.6.1.2.1.43.10.2.1.4.1.1")
26+
TRAY_EMPTY = ("tray_empty", "1.3.6.1.2.1.43.18.1.1.8.1.13", True)
27+
# we observed each printer emitting a different SNMP OID for
28+
# an empty paper tray, the below accounts for this second OID.
29+
# the _2 at the end of this metric does imply that the printer has
30+
# 2 trays. tray_empty_2 is an indication of the same exact issue
31+
# as tray_empty: an empty paper tray.
32+
TRAY_EMPTY_2 = ("tray_empty_2", "1.3.6.1.2.1.43.18.1.1.8.1.2", True)
33+
34+
def __init__(self, metric_name, metric_value, is_error=False):
35+
self.metric_name = metric_name
36+
self.metric_value = metric_value
37+
self.is_error = is_error
38+
39+
40+
def fetch_ips_from_config(config_file_path):
41+
try:
42+
with open(config_file_path, "r") as f:
43+
config = json.load(f)
44+
printer_configs = config.get("PRINTING")
45+
if not printer_configs:
46+
raise Exception("No printers defined in config file")
47+
48+
ip_list = []
49+
for printer in printer_configs:
50+
if isinstance(printer_configs[printer], dict):
51+
ip = printer_configs[printer]["IP"]
52+
logging.info(f"Adding printer {printer} with IP {ip}")
53+
ip_list.append(ip)
54+
return ip_list
55+
56+
except Exception as e:
57+
logging.error(f"error opening config file: {e}")
58+
59+
60+
def scrape_snmp(ip_list, sleep_duration_minutes=5):
61+
while True:
62+
for ip in ip_list:
63+
get_snmp_data(ip)
64+
time.sleep(sleep_duration_minutes * 60)
65+
66+
67+
def get_snmp_data(ip):
68+
for oid in SnmpOid:
69+
with metrics_handler.snmp_request_duration.time():
70+
errorIndication, errorStatus, errorIndex, varBinds = next(
71+
getCmd(
72+
SnmpEngine(),
73+
CommunityData("public", mpModel=0),
74+
UdpTransportTarget((ip, 161)),
75+
ContextData(),
76+
ObjectType(ObjectIdentity(oid.metric_value)),
77+
)
78+
)
79+
if errorIndication:
80+
logging.error(
81+
f"Error indication from {ip} for metric {oid.metric_value}: {errorIndication}"
82+
)
83+
metrics_handler.device_unreachable.set(1)
84+
continue
85+
if errorStatus:
86+
logging.error(
87+
f"Error status from {ip} for metric {oid.metric_value}: {errorStatus.prettyPrint()}"
88+
)
89+
# SNMP OIDs related to errors often dissappear when
90+
# the associated issue that the metric refers to is
91+
# no longer present (i.e. an empty tray now has
92+
# paper). To avoid leaving an error metric as 1
93+
# which would create a false positive, set the metric
94+
# to zero if the associated SNMP OID was not found
95+
if oid.is_error:
96+
metrics_handler.snmp_error.labels(name=oid.metric_name, ip=ip).set(0)
97+
continue
98+
99+
metrics_handler.device_unreachable.set(0)
100+
if not varBinds:
101+
continue
102+
res = varBinds[0]
103+
if oid.is_error:
104+
metrics_handler.snmp_error.labels(name=oid.metric_name, ip=ip).set(1)
105+
continue
106+
metrics_handler.snmp_metric.labels(name=oid.metric_name, ip=ip).set(res[1])

printer/metrics.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,31 @@ class Metrics(enum.Enum):
2323
"total bytes of files pointed to by cache",
2424
prometheus_client.Gauge,
2525
)
26+
SNMP_METRIC = (
27+
"snmp_metric",
28+
"ex: Number of pages printed",
29+
prometheus_client.Gauge,
30+
["name", "ip"],
31+
)
32+
33+
SNMP_ERROR = (
34+
"snmp_error",
35+
"Error metrics",
36+
prometheus_client.Gauge,
37+
["name", "ip"],
38+
)
39+
40+
SNMP_REQ_DURATION = (
41+
"snmp_request_duration",
42+
"Time it took for SNMP request",
43+
prometheus_client.Summary,
44+
)
45+
46+
DEVICE_UNREACHABLE = (
47+
"device_unreachable",
48+
"set to 1 when error",
49+
prometheus_client.Gauge,
50+
)
2651

2752
def __init__(self, title, description, prometheus_type, labels=()):
2853
# we use the above default value for labels because it matches what's used
@@ -38,8 +63,8 @@ class MetricsHandler:
3863
_instance = None
3964

4065
def __init__(self):
41-
raise RuntimeError('Call MetricsHandler.instance() instead')
42-
66+
raise RuntimeError("Call MetricsHandler.instance() instead")
67+
4368
def init(self) -> None:
4469
for metric in Metrics:
4570
setattr(

printer/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ py-grpc-prometheus==0.7.0
44
python-multipart==0.0.9
55
httpx==0.28.1
66
requests==2.32.3
7+
pysnmp==4.4.12
8+
pyasn1==0.4.8

0 commit comments

Comments
 (0)