Skip to content

Commit 4c79cf6

Browse files
committed
fix(users): parse TTY column by header position instead of split index (#989)
1 parent f6bf0d8 commit 4c79cf6

File tree

4 files changed

+43
-15
lines changed

4 files changed

+43
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Monitoring Plugins:
125125
* fs-ro: ignore `/run/credentials` (https://systemd.io/CREDENTIALS/)
126126
* keycloak-stats: fix incorrect symlink for lib
127127
* logfile: fix `OverflowError` when inode exceeds SQLite INTEGER range on Windows/NTFS ([#1035](https://github.com/Linuxfabrik/monitoring-plugins/issues/1035))
128+
* users: fix incorrect TTY count when SSH clients connect via IPv6 ([#989](https://github.com/Linuxfabrik/monitoring-plugins/issues/989))
128129
* ntp-\*: prevent `TypeError: ''=' not supported between instances of 'int' and 'str'`
129130
* valkey-status: fix TLS connection [PR #954](https://github.com/Linuxfabrik/monitoring-plugins/pull/954), thanks to [Claudio Kuenzler](https://github.com/Napsty)
130131

check-plugins/users/unit-test/run

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class TestCheck(unittest.TestCase):
4141
stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(self.check + ' --test=stdout/EXAMPLE01-windows,,0'))
4242
self.assertIn('TTY: 0, PTS: 1', stdout)
4343
self.assertIn('USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME', stdout)
44-
self.assertIn('>linuxfabrik rdp-tcp#0 5 Active . 05.08.2025 10:58', stdout)
44+
# oao() replaces '>' with "'" to prevent HTML injection in Icinga Web
45+
self.assertIn("'linuxfabrik rdp-tcp#0 5 Active . 05.08.2025 10:58", stdout)
4546
self.assertEqual(stderr, '')
4647
self.assertEqual(retc, STATE_OK)
4748

@@ -85,6 +86,13 @@ class TestCheck(unittest.TestCase):
8586
self.assertEqual(stderr, '')
8687
self.assertEqual(retc, STATE_WARN)
8788

89+
def test_if_check_runs_EXAMPLE07_linux_ipv6(self):
90+
"""IPv6 addresses in FROM must not be counted as TTY (#989)."""
91+
stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(self.check + ' --test=stdout/EXAMPLE07-linux,,0'))
92+
self.assertIn('TTY: 0, PTS: 2', stdout)
93+
self.assertEqual(stderr, '')
94+
self.assertEqual(retc, STATE_OK)
95+
8896

8997
if __name__ == '__main__':
9098
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
16:05:00 up 1 day, 2:30, 2 users, load average: 0.10, 0.05, 0.01
2+
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
3+
clox 2001:db8::ff30 16:05 15:44m 0.00s ? sshd: clox [priv]
4+
admin 2001:db8::1 15:30 1:20 0.01s 0.01s -bash

check-plugins/users/users

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@
1111
"""See the check's README for more details.
1212
"""
1313

14-
import argparse # pylint: disable=C0413
15-
import sys # pylint: disable=C0413
14+
import argparse
15+
import sys
1616

17-
import lib.args # pylint: disable=C0413
18-
import lib.base # pylint: disable=C0413
19-
import lib.lftest # pylint: disable=C0413
20-
import lib.shell # pylint: disable=C0413
17+
import lib.args
18+
import lib.base
19+
import lib.lftest
20+
import lib.shell
2121
from lib.globals import (STATE_OK, STATE_UNKNOWN)
2222

2323
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
24-
__version__ = '2025100601'
24+
__version__ = '2026040801'
2525

2626
DESCRIPTION = """Counts how many users are currently logged in, both via tty (on Windows: Console)
2727
and pts (on Linux: typically ssh, on Windows: RDP). Also counts the disconnected
@@ -81,22 +81,37 @@ def parse_args():
8181

8282
def parse_linux_output(s):
8383
"""Parse the output of `w` on Linux.
84+
85+
Uses the header line to determine the TTY column position, so it works
86+
regardless of whether FROM is present, and regardless of column widths
87+
(which vary across distros and versions).
8488
"""
8589
# replace pipes in output, otherwise we will get problems with perfdata,
8690
# and ignore the first line of w's output
8791
s = s.strip().replace('|', '!').splitlines()[1:]
92+
if not s:
93+
return s, 0, 0
94+
95+
header = s[0]
96+
tty_start = header.find('TTY')
97+
if tty_start < 0:
98+
return s, 0, 0
99+
100+
# find end of TTY column: start of the next column header after TTY
101+
after_tty = header[tty_start + 3:]
102+
tty_end = tty_start + 3 + (len(after_tty) - len(after_tty.lstrip()))
103+
88104
count_tty, count_pts = 0, 0
89-
for line in s:
90-
value = line.split()[1]
91-
if 'tty' in value or ':' in value:
92-
# for example, ":0", the 0. host display
93-
# see https://unix.stackexchange.com/questions/16815/what-does-display-0-0-actually-mean
105+
for line in s[1:]:
106+
tty_value = line[tty_start:tty_end].strip() if len(line) > tty_start else ''
107+
if tty_value.startswith('tty') or tty_value.startswith(':'):
108+
# tty = local terminal, ":0" = local X display
94109
count_tty += 1
95110
else:
96-
# this always counts the first header line "USER TTY FROM ...", too
111+
# pts, empty (SSH without TTY) or anything else
97112
count_pts += 1
98113

99-
return s, count_tty, count_pts - 1
114+
return s, count_tty, count_pts
100115

101116

102117
def parse_windows_output(s):

0 commit comments

Comments
 (0)