Technical Architecture¶
This document describes how Zino is structured internally. Understanding the architecture helps when debugging issues, extending functionality, or integrating with other systems.
High-Level Overview¶
flowchart TB
subgraph Routers["Routers (SNMP devices)"]
end
Routers --> Traps["SNMP Traps<br/>(trapd)"]
Routers --> Polling["SNMP Polling<br/>(tasks)"]
Routers -.->|state queries| State
Traps --> State["ZinoState<br/>(events, devices)"]
Polling --> State
State --> Port8001["Port 8001<br/>(command)"]
State --> Port8002["Port 8002<br/>(notify)"]
State --> StateFile["State File<br/>(JSON)"]
Port8001 --> Clients["Client UIs<br/>(curitz, ritz, etc.)"]
Port8002 --> Clients
Components¶
Entry Point¶
The main entry point is zino.zino:main(), invoked by the zino console
script. On startup, it:
Parses command-line arguments
Loads configuration (
zino.toml, if present)Initializes the SNMP backend
Loads persisted state from the JSON file
Starts the async event loop
Schedules polling tasks for all configured devices
Starts the API server and trap listener
State Management¶
ZinoState (in zino.state) is the central container for all runtime
data:
devices: Cached state for each monitored router
events: All active and recently-closed events
addresses: IP-to-hostname reverse mapping
planned_maintenances: Active maintenance windows
flapping: Flap detection state per interface
State is persisted to a JSON file periodically (default: every 5 minutes) and on shutdown. The file format is a direct JSON serialization of the Pydantic models.
Task System¶
Polling is implemented through tasks - async functions scheduled to run
periodically for each device. The base class Task (in zino.tasks.task)
provides common functionality; concrete tasks implement the run() method.
- ReachableTask
Checks basic SNMP connectivity. If a device becomes unreachable, creates a reachability event. This runs first to avoid pointless polling of unreachable devices.
- LinkStateTask
Polls interface table (IF-MIB) to detect link state changes. Compares current state against cached state to identify transitions.
- BGPStateMonitorTask
Polls BGP peer tables to detect session state changes. Supports multiple BGP styles (Juniper, Cisco, generic MIBs).
- BFDTask
Polls BFD session state. BFD provides sub-second failure detection, and Zino tracks when sessions leave the “up” state.
- JuniperAlarmTask
Polls Juniper-specific chassis alarm MIBs. Only runs on devices identified as Juniper equipment.
Tasks are scheduled using APScheduler, with intervals defined per-device in
polldevs.cf.
SNMP Subsystem¶
Zino supports two SNMP backends:
- Net-SNMP (default)
Uses the
netsnmpPython bindings to the Net-SNMP C library. Faster and more mature, but requires system libraries to be installed.- PySNMP
Pure Python implementation. Easier to install (pip only) but somewhat slower. Useful when Net-SNMP isn’t available.
The backend is selected via configuration and abstracted behind a common
interface (zino.snmp), so tasks don’t need to know which backend is in use.
Trap Handling¶
The trap daemon (zino.trapd) listens for incoming SNMP traps on a
configured port (default: 162, or 1162 for non-root operation).
When a trap arrives, it’s dispatched to trap observers - handler functions registered for specific trap types:
link_traps: Handles linkUp/linkDown notificationsbgp_traps: Handles BGP state change trapsbfd_traps: Handles BFD session trapslogged_traps: Logs traps for debuggingignored_traps: Explicitly ignores known-noisy traps
Trap observers typically trigger immediate polling to confirm the reported state change before creating or updating events.
API Server¶
The API server (zino.api) provides two TCP interfaces:
- Command Interface (port 8001)
Implements the legacy Zino protocol - a text-based, SMTP-like protocol where clients send commands and receive responses. Commands include:
CASEIDS: List all event IDsGETATTRS <id>: Get event attributesSETSTATE <id> <state>: Change event stateADDHIST <id>: Add history entryPM ADD/LIST/CANCEL: Manage planned maintenance
This protocol is preserved exactly from Zino 1 for backwards compatibility with existing clients.
- Notification Interface (port 8002)
A simpler protocol for push notifications. Clients connect and authenticate, then receive messages whenever events are created or modified. Message format:
%%<id> <state> <type>
Clients use this to maintain live views without polling.
Authentication¶
Both API ports require authentication. Credentials are stored in a secrets
file (configurable path), one username/password pair per line.
The authentication protocol supports both simple password and challenge-response mechanisms, preventing password replay attacks.
Event System¶
The Events container (zino.events) manages the event lifecycle:
Creating events: Tasks call
get_or_create_event()when detecting a state change. If an event already exists for that (router, subindex, type) tuple, it’s returned; otherwise, a new one is created.Updating events: Events are “checked out” for modification, then “committed” back. This pattern allows atomic updates.
Observers: Components can register as event observers to be notified when events change. The notification API uses this to push updates to clients.
Archiving: Closed events are eventually archived to the
old-events/directory and removed from active state.
Data Flow Example¶
Here’s how a typical link-down scenario flows through the system:
Trap arrives: Router sends linkDown trap to port 162
Trap dispatch:
link_trapsobserver receives the trapImmediate poll: Observer triggers a poll of the affected interface
State comparison:
LinkStateTaskcompares new state against cacheEvent creation: Task calls
get_or_create_event()for a portstate eventEvent committed: New event is added to
ZinoState.eventsObserver notification: Event observers are called
Client notification: Notification server pushes update to connected clients
State persistence: Next periodic dump saves the new event to JSON
Configuration Files¶
- polldevs.cf
Required. Lists devices to monitor with their SNMP credentials and polling intervals. Zino watches this file for changes and reloads automatically.
- zino.toml
Optional. Configures persistence paths, logging, SNMP backend selection, and other operational parameters.
- secrets
Required for API access. Contains username/password pairs for client authentication.
Concurrency Model¶
Zino uses Python’s asyncio for concurrency:
Polling tasks run as coroutines, allowing many devices to be polled concurrently without threads
The API server handles multiple client connections concurrently
SNMP operations are async (with appropriate backend support)
APScheduler manages task timing, triggering coroutines at configured intervals. The scheduler runs in the same event loop as the API server and trap listener.
Extending Zino¶
- Adding a new task type
Create a new class inheriting from
Task, implement therun()method, and register it in the scheduler setup inget_registered_tasksinzino/tasks/__init__.py.- Adding a new trap observer
Create a module in
zino/trapobservers/, define handler functions with appropriate decorators, and import the module inzino.py.- Adding new event types
Define a new event class in
zino.statemodelsinheriting fromEvent, add it to the type union inEvents, and create corresponding tasks or trap observers.