Skip to content

Commit abf012c

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

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
12+
<leafNode name="module">
13+
<properties>
14+
<help>Kernel module to load for watchdog device (optional)</help>
15+
<valueHelp>
16+
<format>txt</format>
17+
<description>Module name (e.g. 'softdog', 'iTCO_wdt', 'sp5100_tco')</description>
18+
</valueHelp>
19+
<constraint>
20+
<regex>[a-zA-Z0-9_\-]+</regex>
21+
</constraint>
22+
<constraintErrorMessage>Module name must be alphanumeric/underscore/hyphen</constraintErrorMessage>
23+
</properties>
24+
</leafNode>
25+
<leafNode name="timeout">
26+
<properties>
27+
<help>Watchdog timeout for runtime in seconds (1-86400)</help>
28+
<valueHelp>
29+
<format>u32:1-86400</format>
30+
<description>Seconds</description>
31+
</valueHelp>
32+
<constraint>
33+
<validator name="numeric" argument="--range 1-86400"/>
34+
</constraint>
35+
<constraintErrorMessage>Timeout must be between 1 and 86400 seconds</constraintErrorMessage>
36+
</properties>
37+
<defaultValue>10</defaultValue>
38+
</leafNode>
39+
<leafNode name="shutdown-timeout">
40+
<properties>
41+
<help>Watchdog timeout during shutdown in seconds (1-86400)</help>
42+
<valueHelp>
43+
<format>u32:1-86400</format>
44+
<description>Seconds</description>
45+
</valueHelp>
46+
<constraint>
47+
<validator name="numeric" argument="--range 1-86400"/>
48+
</constraint>
49+
<constraintErrorMessage>Shutdown timeout must be between 1 and 86400 seconds</constraintErrorMessage>
50+
</properties>
51+
<defaultValue>120</defaultValue>
52+
</leafNode>
53+
<leafNode name="reboot-timeout">
54+
<properties>
55+
<help>Watchdog timeout during reboot in seconds (1-86400)</help>
56+
<valueHelp>
57+
<format>u32:1-86400</format>
58+
<description>Seconds</description>
59+
</valueHelp>
60+
<constraint>
61+
<validator name="numeric" argument="--range 1-86400"/>
62+
</constraint>
63+
<constraintErrorMessage>Reboot timeout must be between 1 and 86400 seconds</constraintErrorMessage>
64+
</properties>
65+
<defaultValue>120</defaultValue>
66+
</leafNode>
67+
</children>
68+
</node>
69+
</children>
70+
</node>
71+
</interfaceDefinition>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
51+
if __name__ == '__main__':
52+
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())

src/conf_mode/system_watchdog.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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
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+
33+
34+
def get_config(config=None):
35+
if config:
36+
conf = config
37+
else:
38+
conf = Config()
39+
base = ['system', 'watchdog']
40+
41+
if not conf.exists(base):
42+
return None
43+
44+
watchdog = conf.get_config_dict(
45+
base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True
46+
)
47+
48+
return watchdog
49+
50+
51+
def verify(watchdog):
52+
if watchdog is None:
53+
return None
54+
55+
56+
def generate(watchdog):
57+
# If watchdog node removed entirely, clean up everything
58+
if watchdog is None:
59+
watchdog_config_file.unlink(missing_ok=True)
60+
modules_load_file.unlink(missing_ok=True)
61+
return None
62+
63+
# Persist kernel module autoload on boot if specified (even if not enabled)
64+
module = watchdog.get('module')
65+
if module:
66+
try:
67+
modules_load_directory.mkdir(exist_ok=True)
68+
modules_load_file.write_text(f"{module}\n")
69+
except Exception as e:
70+
print(f"Warning: Failed writing modules-load configuration: {e}")
71+
else:
72+
# If module option removed, drop persisted autoload file
73+
modules_load_file.unlink(missing_ok=True)
74+
75+
# Try to load kernel module if specified and /dev/watchdog0 is missing
76+
if not Path('/dev/watchdog0').exists():
77+
if module:
78+
# Try to load the module using vyos call wrapper for logging/airbag integration
79+
try:
80+
call(f'modprobe {module}')
81+
except Exception as e:
82+
print(f"Warning: Could not load watchdog module '{module}': {e}")
83+
# Re-check for device
84+
if not Path('/dev/watchdog0').exists():
85+
print(
86+
"Warning: /dev/watchdog0 not found. Systemd watchdog will not be enabled."
87+
)
88+
watchdog_config_file.unlink(missing_ok=True)
89+
return None
90+
91+
# Ensure the directory exists
92+
watchdog_config_dir.mkdir(parents=True, exist_ok=True)
93+
94+
# Pass through configured time values directly as seconds
95+
render(str(watchdog_config_file), 'system/watchdog.conf.j2', watchdog)
96+
97+
return None
98+
99+
100+
def apply(watchdog):
101+
# Reload systemd daemon to apply/unload the watchdog configuration
102+
# The watchdog settings take immediate effect after systemd is reloaded
103+
call('systemctl daemon-reload')
104+
105+
return None
106+
107+
108+
if __name__ == '__main__':
109+
try:
110+
c = get_config()
111+
verify(c)
112+
generate(c)
113+
apply(c)
114+
except ConfigError as e:
115+
print(e)
116+
exit(1)

0 commit comments

Comments
 (0)