Skip to content

Commit 811bee9

Browse files
committed
T7101: Add support for hardware watchdog support via systemd
1 parent b50808a commit 811bee9

File tree

4 files changed

+275
-0
lines changed

4 files changed

+275
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Autogenerated by system_watchdog.py ###
2+
[Manager]
3+
RuntimeWatchdogSec={{ timeout }}
4+
ShutdownWatchdogSec={{ shutdown_timeout }}
5+
RebootWatchdogSec={{ reboot_timeout }}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0"?>
2+
<interfaceDefinition>
3+
<node name="system">
4+
<children>
5+
<node name="watchdog" owner="${vyos_conf_scripts_dir}/system_watchdog.py">
6+
<properties>
7+
<help>Hardware watchdog configuration</help>
8+
<priority>9999</priority>
9+
</properties>
10+
<children>
11+
<leafNode name="module">
12+
<properties>
13+
<help>Kernel module to load for watchdog device (optional)</help>
14+
<valueHelp>
15+
<format>txt</format>
16+
<description>Module name (e.g. 'softdog', 'iTCO_wdt', 'sp5100_tco')</description>
17+
</valueHelp>
18+
<constraint>
19+
<regex>[a-zA-Z0-9_\-]+</regex>
20+
</constraint>
21+
<constraintErrorMessage>Module name must be alphanumeric/underscore/hyphen</constraintErrorMessage>
22+
</properties>
23+
</leafNode>
24+
<leafNode name="timeout">
25+
<properties>
26+
<help>Watchdog timeout for runtime in seconds (1-86400)</help>
27+
<valueHelp>
28+
<format>u32:1-86400</format>
29+
<description>Seconds</description>
30+
</valueHelp>
31+
<constraint>
32+
<validator name="numeric" argument="--range 1-86400"/>
33+
</constraint>
34+
<constraintErrorMessage>Timeout must be between 1 and 86400 seconds</constraintErrorMessage>
35+
</properties>
36+
<defaultValue>10</defaultValue>
37+
</leafNode>
38+
<leafNode name="shutdown-timeout">
39+
<properties>
40+
<help>Watchdog timeout during shutdown in seconds (1-86400)</help>
41+
<valueHelp>
42+
<format>u32:60-86400</format>
43+
<description>Seconds</description>
44+
</valueHelp>
45+
<constraint>
46+
<validator name="numeric" argument="--range 60-86400"/>
47+
</constraint>
48+
<constraintErrorMessage>Shutdown timeout must be between 60 and 86400 seconds</constraintErrorMessage>
49+
</properties>
50+
<defaultValue>120</defaultValue>
51+
</leafNode>
52+
<leafNode name="reboot-timeout">
53+
<properties>
54+
<help>Watchdog timeout during reboot in seconds (60-86400)</help>
55+
<valueHelp>
56+
<format>u32:60-86400</format>
57+
<description>Seconds</description>
58+
</valueHelp>
59+
<constraint>
60+
<validator name="numeric" argument="--range 60-86400"/>
61+
</constraint>
62+
<constraintErrorMessage>Reboot timeout must be between 60 and 86400 seconds</constraintErrorMessage>
63+
</properties>
64+
<defaultValue>120</defaultValue>
65+
</leafNode>
66+
</children>
67+
</node>
68+
</children>
69+
</node>
70+
</interfaceDefinition>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
3+
#
4+
# This program is free software; you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License version 2 or later as
6+
# published by the Free Software Foundation.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
import os
17+
import unittest
18+
19+
from base_vyostest_shim import VyOSUnitTestSHIM
20+
from vyos.utils.process import cmd
21+
22+
base_path = ['system', 'watchdog']
23+
24+
25+
class TestSystemWatchdog(VyOSUnitTestSHIM.TestCase):
26+
def tearDown(self):
27+
self.cli_delete(base_path)
28+
self.cli_commit()
29+
super().tearDown()
30+
31+
def test_enable_watchdog_softdog(self):
32+
"""Configure watchdog (presence enables) with softdog and check state"""
33+
# Presence of 'system watchdog' enables watchdog; set module to softdog
34+
self.cli_set(base_path)
35+
self.cli_set(base_path + ['module', 'softdog'])
36+
self.cli_commit()
37+
# Check if softdog module is loaded
38+
lsmod = cmd('lsmod')
39+
self.assertIn('softdog', lsmod)
40+
# Check /dev/watchdog0 exists
41+
self.assertTrue(
42+
os.path.exists('/dev/watchdog0'), '/dev/watchdog0 does not exist'
43+
)
44+
# Check systemd config file exists
45+
config_path = '/run/systemd/system.conf.d/watchdog.conf'
46+
self.assertTrue(
47+
os.path.exists(config_path), f"Systemd config file not found: {config_path}"
48+
)
49+
50+
def test_invalid_module_rejected(self):
51+
"""Verify that a non-existent watchdog module causes commit failure"""
52+
# Choose a module name unlikely to exist; include a prefix to avoid collision with real names
53+
bogus_module = 'zzzx_watchdog_unit_test_fake'
54+
self.cli_set(base_path)
55+
self.cli_set(base_path + ['module', bogus_module])
56+
# Commit should fail due to verify() raising ConfigError on modprobe dry-run failure
57+
with self.assertRaisesRegex(Exception, r"Watchdog module '.*' was not found"):
58+
self.cli_commit()
59+
60+
61+
if __name__ == '__main__':
62+
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())

src/conf_mode/system_watchdog.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License version 2 or later as
7+
# published by the Free Software Foundation.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
from sys import exit
18+
from pathlib import Path
19+
20+
from vyos.config import Config
21+
from vyos.template import render
22+
from vyos.utils.process import call, run
23+
from vyos import ConfigError
24+
from vyos import airbag
25+
26+
airbag.enable()
27+
28+
watchdog_config_dir = Path('/run/systemd/system.conf.d')
29+
watchdog_config_file = Path(watchdog_config_dir / 'watchdog.conf')
30+
modules_load_directory = Path('/run/modules-load.d')
31+
modules_load_file = Path(modules_load_directory / 'watchdog.conf')
32+
WATCHDOG_DEV = Path('/dev/watchdog0')
33+
34+
35+
def get_config(config=None):
36+
if config:
37+
conf = config
38+
else:
39+
conf = Config()
40+
base = ['system', 'watchdog']
41+
42+
if not conf.exists(base):
43+
return None
44+
45+
watchdog = conf.get_config_dict(
46+
base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True
47+
)
48+
49+
return watchdog
50+
51+
52+
def verify(watchdog):
53+
if watchdog is None:
54+
return None
55+
56+
module = watchdog.get('module')
57+
device_exists = WATCHDOG_DEV.exists()
58+
59+
# Require a usable watchdog: either device already present or a module provided
60+
if not module and not device_exists:
61+
raise ConfigError(
62+
"No watchdog device found at /dev/watchdog0 and no module configured. "
63+
"Either configure 'system watchdog module <name>' or ensure the kernel auto-loads a watchdog driver."
64+
)
65+
66+
# If a module is provided, verify it resolves via modprobe (dry-run)
67+
if module:
68+
# Dry-run modprobe (-n) in quiet mode (-q) verifies availability without loading
69+
rc = run(f'modprobe -n -q {module}')
70+
if rc != 0:
71+
raise ConfigError(
72+
f"Watchdog module '{module}' was not found or cannot be loaded"
73+
)
74+
75+
return None
76+
77+
78+
def generate(watchdog):
79+
# If watchdog node removed entirely, clean up everything
80+
if watchdog is None:
81+
watchdog_config_file.unlink(missing_ok=True)
82+
modules_load_file.unlink(missing_ok=True)
83+
return None
84+
85+
# Persist kernel module autoload on boot if specified (even if not enabled)
86+
module = watchdog.get('module')
87+
if module:
88+
try:
89+
modules_load_directory.mkdir(exist_ok=True)
90+
modules_load_file.write_text(f"{module}\n")
91+
except Exception as e:
92+
print(f"Warning: Failed writing modules-load configuration: {e}")
93+
else:
94+
# If module option removed, drop persisted autoload file
95+
modules_load_file.unlink(missing_ok=True)
96+
97+
# Try to load kernel module if specified and /dev/watchdog0 is missing
98+
if not WATCHDOG_DEV.exists():
99+
if module:
100+
# Try to load the module using vyos call wrapper for logging/airbag integration
101+
try:
102+
call(f'modprobe {module}')
103+
except Exception as e:
104+
print(f"Warning: Could not load watchdog module '{module}': {e}")
105+
# Re-check for device
106+
if not WATCHDOG_DEV.exists():
107+
print(
108+
"Warning: /dev/watchdog0 not found. Systemd watchdog will not be enabled."
109+
)
110+
watchdog_config_file.unlink(missing_ok=True)
111+
return None
112+
113+
# Ensure the directory exists
114+
watchdog_config_dir.mkdir(parents=True, exist_ok=True)
115+
116+
# Pass through configured time values directly as seconds
117+
render(str(watchdog_config_file), 'system/watchdog.conf.j2', watchdog)
118+
119+
return None
120+
121+
122+
def apply(watchdog):
123+
# Reload systemd daemon to apply/unload the watchdog configuration
124+
# The watchdog settings take immediate effect after systemd is reloaded
125+
call('systemctl daemon-reload')
126+
127+
return None
128+
129+
130+
if __name__ == '__main__':
131+
try:
132+
c = get_config()
133+
verify(c)
134+
generate(c)
135+
apply(c)
136+
except ConfigError as e:
137+
print(e)
138+
exit(1)

0 commit comments

Comments
 (0)