Configuring Zino

Minimal configuration

At minimum, Zino must be configured with a list of SNMP-enabled routers to monitor, the polldevs.cf file, and a list of users, the secrets file.

By default, it looks for polldevs.cf in the current working directory, but a different configuration file can be specified using the --polldevs command line option.

See polldevs.cf.example file for an example of the configuration format, reproduced below:

# List of routers for Zino to monitor.
#
# An empty line signifies the start of a new configuration block, i.e. a set of
# defaults, or a new, distinct router.  Router names can be arbitrary, but must
# be unique across all of Zino.  Comments begin with the # character.

# The following block sets default values that will apply to all router
# entries.  Any of these can be overridden in individual router entries:
#
# interval is a number of *minutes* between polling jobs for a router.
default interval: 5
default community: public
default domain: example.org
default snmpversion: v2c

# Example router, with a higher scheduling priority.  Interfaces whose name
# matches the regular expression `pe-` will be not be monitored:
name: example-gw
address: 192.168.42.1
priority: 500
ignorepat: pe-

# Only interfaces that match the regular expression `ge-1\/` on example-gw2
# will be monitored:
name: example-gw2
address: 192.168.42.2
watchpat: ge-1\/

# example-gw3 will be polled every 2 minutes, rather than the default 5
name: example-gw3
address: 192.168.42.3
interval: 2

# example-gw4 uses a different SNMP community than the default setting:
name: example-gw4
address: 192.168.42.4
community: foobar

Zino will check polldevs.cf for changes on a scheduled interval while it’s running, so any changes made while Zino is running should be picked up without requiring a restart of the process.

The secrets file is of a much simpler format, see Configuring API users. If it is readable for other users than the one Zino runs as, Zino will log a warning. This file is read on every log in.

Configuring other settings

Other settings can be also configured in a separate TOML file, which defaults to zino.toml in the current working directory, but a different file can be specified using the --config-file command line option.

See the zino.toml.example file for the settings that can be configured and their default values, reproduced below:

[archiving]
# Path to directory where dumps of expired events are saved
# default "old-events"
old_events_dir = "old-events"

[authentication]
# Path to the file containing user accounts and secrets for the Zino API
# default "secrets"
file = "secrets"

[persistence]
# Path to where the persistent Zino state should be saved
# default "zino-state.json"
file = "zino-state.json"

# How often the persistent Zino state should be saved, in minutes
# default 5 min
period = 5

[polling]
# Path to a pollfile, the list of routers to monitor and related configuration
# default "polldevs.cf"
file = "polldevs.cf"

# How often the pollfile is checked for changes, in minutes
# default 1 min
period = 1

[process]
# User to switch to after binding to privileged ports.  When Zino starts as root, it will drop privileges to
# this user once protected ports (like the SNMP trap port 162) are bound.  Can be overridden by --user on the
# command line.  Default: not set (no privilege dropping).
# user = "zino"

[snmp]
# Zino supports two SNMP back-ends: "netsnmp" and "pysnmp".  The default is "netsnmp" (which requires the Net-SNMP C
# library to be installed on your system).  "pysnmp" selects the pure Python library PySNMP, which is less performant.
backend = "netsnmp"

[snmp.trap]
# Only accept SNMP trap messages with any community string in this list. The default value is [].
# The netsnmp backend will accept an empty list to mean: Admit any trap message, regardless of community.  The pysnmp
# backend will not function properly unless there is at least one community string in the list.
require_community = []

# Trap source mode.  "direct" (default) means Zino binds its own UDP port for trap reception.
# "straps" connects to a straps UNIX domain socket, and "nmtrapd" connects to an nmtrapd TCP socket.
# Using a trap multiplexer allows Zino to receive traps without starting as root.
# source = "direct"

# Path to the straps UNIX socket (only used when source = "straps").
# Default: "/tmp/.straps-162"
# straps_socket = "/tmp/.straps-162"

# Host and port for nmtrapd TCP connection (only used when source = "nmtrapd").
# nmtrapd_host = "localhost"
# nmtrapd_port = 1702

[snmp.agent]
# SNMP agent configuration for responding to uptime queries from clients
# This agent is used by clients like EMT for failover detection
enabled = true  # Enable/disable the SNMP agent (default: true)
port = 8000  # Port to listen on (default: 8000)
address = "0.0.0.0"  # Address to bind to (default: "0.0.0.0")
# community = "public"  # SNMP community string to accept (default: public)

[scheduler]
# When Zino is very busy, some scheduled recurring jobs may be delayed from running on their exactly scheduled
# time.  This setting controls how delayed a job is allowed to be and still be run.  The default is 10 seconds.
# misfire_grace_time = 10

[event]
# Whether to create portstate events for newly discovered interfaces that are operationally down
make_events_for_new_interfaces = false

# Whether to create reachability events for newly discovered devices to indicate that they were found by Zino
make_events_for_new_devices = false

# Logging configuration is optional, but if specified, it will override the
# default logging config of Zino.  This is a TOML representation of the default
# logging configuration dictionary, whose full documentation is available at
# https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
#
#[logging]
#version = 1
#disable_existing_loggers = false
#
#[logging.loggers.root]
#level = "INFO"
#handlers = ["console"]
#
#[logging.loggers.apscheduler]
#level = "WARNING"
#
#[logging.formatters.standard]
#format = "%(asctime)s - %(levelname)s - %(name)s (%(threadName)s) - %(message)s"
#
#[logging.handlers.console]
#class = "logging.StreamHandler"
#formatter = "standard"
#stream = "ext://sys.stderr"

Zino does not currently check zino.toml for changes on a scheduled interval while it’s running, so Zino needs to be restarted for changes to take effect.

Process configuration

The [process] section controls process-level behavior.

[process]
user = "zino"
user

Username to switch to after binding to privileged ports. When Zino starts as root, it will drop privileges to this user once protected ports (like the SNMP trap port 162) are bound. This can be overridden by the --user command-line option. Default: not set (no privilege dropping).

Configuring trap reception

By default, Zino binds directly to a UDP port (default 162) to receive SNMP traps. This requires Zino to start as root (or with the CAP_NET_BIND_SERVICE capability on Linux).

Alternatively, Zino can receive traps through an external SNMP trap multiplexer. A trap multiplexer can run as root and bind to port 162, then re-broadcasts incoming raw trap packets to connected clients. This allows Zino (and an arbitrary number of other programs) to receive traps without having elevated privileges.

Two multiplexer protocols are supported:

straps

The original trap multiplexer from the Scotty project (version 2.1.x). It creates a UNIX domain socket at /tmp/.straps-<port> (e.g. /tmp/.straps-162) and forwards framed trap packets to all connected clients. The straps source can be found in the scotty 2.1.11 tarball at tnm/snmp/straps.c — it is a self-contained C program with no external dependencies.

nmtrapd

The newer variant from FlightAware’s scotty fork. It uses a TCP socket on localhost port 1702 instead of a UNIX domain socket. The source is at tnm/unix/nmtrapd.c in the repository.

To use a trap multiplexer, configure the [snmp.trap] section:

zino.toml
[snmp.trap]
source = "straps"
# straps_socket = "/tmp/.straps-162"  # default

For nmtrapd:

zino.toml
[snmp.trap]
source = "nmtrapd"
# nmtrapd_host = "localhost"  # default
# nmtrapd_port = 1702  # default

When using a multiplexer, the --trap-port command-line option is ignored. Zino will automatically reconnect if the multiplexer connection is lost, and includes a watchdog that detects silent connections (inspired by Zino 1’s TrapWatchdog).

Configuring logging

Zino uses the logging framework provided by the Python standard library. Most aspects of how Zino handles logging can also be controlled through zino.toml. Specifically, Zino automatically feeds everything under the logging section of zino.toml to Python’s logging.config.dictConfig(). For a complete overview of which options exist, please refer to Python’s documentation of the configuration dictionary schema. The Zino example config includes comments that show Zino’s default logging setup.

Zino’s log output is organized into a hierarchy of loggers that correspond to the internal Python module hierarchy of Zino, which means that the log level of different parts of Zino can be controlled individually. If, for example, you specifically wanted the reachability task to log debug message, you could add this to the configuration:

zino.toml
 [logging.loggers."zino.tasks.reachabletask"]
 level = "DEBUG"

A more complex example could be to specifically output all kinds of debug-level information from netsnmp-cffi and the Net-SNMP C library to a separate file. Due to the sheer volume of debug logs, it could even be desirable to enable automatic log rotation every time the log file exceeds 1GB in size:

zino.toml
 # Separate file handler for netsnmpy debug logs
 [logging.handlers.netsnmp_file]
 class = "logging.handlers.RotatingFileHandler"
 formatter = "standard"
 filename = "netsnmp-debug.log"
 maxBytes = 1073741824  # 1GB
 # Keep 3 backup files
 backupCount = 3

 # Send netsnmpy debug logs to a separate file to avoid console spam
 [logging.loggers.netsnmpy]
 level = "DEBUG"
 handlers = ["netsnmp_file"]
 # Avoid duplicate log message by disabling propagation to the root logger
 propagate = false

Tip

Zino can also be manually made to log its list of currently executing polling jobs (including their start times and runtime duration) by sending it the USR1 signal to a running Zino process, for example by using a command like pkill -SIGUSR1 zino.

Configuring API users

Zino 2 reimplements the text-based (vaguely SMTP-esque) API protocol from Zino 1, warts and all. This means that the protocol runs over unencrypted TCP sessions. Access to restricted API information requires authentication through the USER command. Usernames and passwords are configured in cleartext in a secrets file, e.g.:

user1 password123
user2 my-pets-name

You should therefore ensure that the secrets file is only readable for the user that the zino command runs as.

Please note that passwords are not transmitted in cleartext over API socket connections. The Zino server protocol utilizes a challenge-response mechanism, in which the user logging in must prove that they know the password by giving a correct response to the given challenge.

When opening a connection to the API port, the Zino server will immediately send a hello message with a session challenge included:

$ telnet localhost 8001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
200 6077fe9fa53e4921b35c11cf6ef8891bc0194875 Hello, there

To authenticate properly, the client must issue the USER command, which has two arguments: A username and a challenge response string. Given the challenge value from above (6077fe9fa53e4921b35c11cf6ef8891bc0194875), the proper challenge response for user1 can be computed on the command line thus:

$ echo -n "6077fe9fa53e4921b35c11cf6ef8891bc0194875 password123" | sha1sum
4daf3c1448c2c4b3b92489024cc4676f70c26b1d  -
$

The proper way to authenticate as user1 would then be to issue this command:

USER user1 4daf3c1448c2c4b3b92489024cc4676f70c26b1d