Skip to content

Commit 4a5ff9b

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

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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="enable">
12+
<properties>
13+
<help>Enable hardware watchdog</help>
14+
<valueless/>
15+
</properties>
16+
</leafNode>
17+
<leafNode name="module">
18+
<properties>
19+
<help>Kernel module to load for watchdog device (optional)</help>
20+
<valueHelp>
21+
<format>txt</format>
22+
<description>Module name (e.g. 'softdog', 'iTCO_wdt', 'sp5100_tco')</description>
23+
</valueHelp>
24+
<constraint>
25+
<regex>[a-zA-Z0-9_\-]+</regex>
26+
</constraint>
27+
<constraintErrorMessage>Module name must be alphanumeric/underscore/hyphen</constraintErrorMessage>
28+
</properties>
29+
</leafNode>
30+
<leafNode name="timeout">
31+
<properties>
32+
<help>Watchdog timeout for runtime (default 10 seconds)</help>
33+
<valueHelp>
34+
<format>u32:1-2147483647</format>
35+
<description>Timeout in seconds</description>
36+
</valueHelp>
37+
<valueHelp>
38+
<format>&lt;number&gt;s</format>
39+
<description>Timeout in seconds (e.g. 30s)</description>
40+
</valueHelp>
41+
<valueHelp>
42+
<format>&lt;number&gt;m</format>
43+
<description>Timeout in minutes (e.g. 5m)</description>
44+
</valueHelp>
45+
<valueHelp>
46+
<format>&lt;number&gt;min</format>
47+
<description>Timeout in minutes (e.g. 5min)</description>
48+
</valueHelp>
49+
<valueHelp>
50+
<format>&lt;number&gt;h</format>
51+
<description>Timeout in hours (e.g. 1h)</description>
52+
</valueHelp>
53+
<constraint>
54+
<regex>([1-9][0-9]*|[1-9][0-9]*(s|m|min|h))</regex>
55+
</constraint>
56+
<constraintErrorMessage>Timeout must be a positive number optionally followed by s, m, min, or h</constraintErrorMessage>
57+
</properties>
58+
<defaultValue>10</defaultValue>
59+
</leafNode>
60+
<leafNode name="shutdown-timeout">
61+
<properties>
62+
<help>Watchdog timeout during shutdown (default 2 minutes)</help>
63+
<valueHelp>
64+
<format>u32:1-2147483647</format>
65+
<description>Timeout in seconds</description>
66+
</valueHelp>
67+
<valueHelp>
68+
<format>&lt;number&gt;s</format>
69+
<description>Timeout in seconds (e.g. 30s)</description>
70+
</valueHelp>
71+
<valueHelp>
72+
<format>&lt;number&gt;m</format>
73+
<description>Timeout in minutes (e.g. 5m)</description>
74+
</valueHelp>
75+
<valueHelp>
76+
<format>&lt;number&gt;min</format>
77+
<description>Timeout in minutes (e.g. 5min)</description>
78+
</valueHelp>
79+
<valueHelp>
80+
<format>&lt;number&gt;h</format>
81+
<description>Timeout in hours (e.g. 1h)</description>
82+
</valueHelp>
83+
<constraint>
84+
<regex>([1-9][0-9]*|[1-9][0-9]*(s|m|min|h))</regex>
85+
</constraint>
86+
<constraintErrorMessage>Timeout must be a positive number optionally followed by s, m, min, or h</constraintErrorMessage>
87+
</properties>
88+
<defaultValue>2min</defaultValue>
89+
</leafNode>
90+
<leafNode name="reboot-timeout">
91+
<properties>
92+
<help>Watchdog timeout during reboot (default 2 minutes)</help>
93+
<valueHelp>
94+
<format>u32:1-2147483647</format>
95+
<description>Timeout in seconds</description>
96+
</valueHelp>
97+
<valueHelp>
98+
<format>&lt;number&gt;s</format>
99+
<description>Timeout in seconds (e.g. 30s)</description>
100+
</valueHelp>
101+
<valueHelp>
102+
<format>&lt;number&gt;m</format>
103+
<description>Timeout in minutes (e.g. 5m)</description>
104+
</valueHelp>
105+
<valueHelp>
106+
<format>&lt;number&gt;min</format>
107+
<description>Timeout in minutes (e.g. 5min)</description>
108+
</valueHelp>
109+
<valueHelp>
110+
<format>&lt;number&gt;h</format>
111+
<description>Timeout in hours (e.g. 1h)</description>
112+
</valueHelp>
113+
<constraint>
114+
<regex>([1-9][0-9]*|[1-9][0-9]*(s|m|min|h))</regex>
115+
</constraint>
116+
<constraintErrorMessage>Timeout must be a positive number optionally followed by s, m, min, or h</constraintErrorMessage>
117+
</properties>
118+
<defaultValue>2min</defaultValue>
119+
</leafNode>
120+
</children>
121+
</node>
122+
</children>
123+
</node>
124+
</interfaceDefinition>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
# Copyright VyOS maintainers and contributors <[email protected]>
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.configsession import ConfigSessionError
21+
from vyos.utils.file import read_file
22+
from vyos.utils.process import cmd
23+
24+
base_path = ['system', 'watchdog']
25+
26+
27+
class TestSystemWatchdog(VyOSUnitTestSHIM.TestCase):
28+
def tearDown(self):
29+
self.cli_delete(base_path)
30+
self.cli_commit()
31+
super().tearDown()
32+
33+
def test_enable_watchdog_softdog(self):
34+
"""Enable watchdog with softdog module and check system state"""
35+
self.cli_set(base_path + ['enable'])
36+
self.cli_set(base_path + ['module', 'softdog'])
37+
self.cli_commit()
38+
# Check if softdog module is loaded
39+
lsmod = cmd('lsmod')
40+
self.assertIn('softdog', lsmod)
41+
# Check /dev/watchdog0 exists
42+
self.assertTrue(
43+
os.path.exists('/dev/watchdog0'), '/dev/watchdog0 does not exist'
44+
)
45+
# Check systemd config file exists
46+
config_path = '/etc/systemd/system/watchdog.service'
47+
self.assertTrue(
48+
os.path.exists(config_path), f"Systemd config file not found: {config_path}"
49+
)
50+
51+
52+
if __name__ == '__main__':
53+
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())

src/conf_mode/system_watchdog.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright VyOS maintainers and contributors <[email protected]>
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+
import os
18+
19+
from sys import exit
20+
21+
from vyos.config import Config
22+
from vyos.template import render
23+
from vyos.utils.file import write_file
24+
from vyos.utils.process import cmd
25+
from vyos import ConfigError
26+
from vyos import airbag
27+
28+
airbag.enable()
29+
30+
watchdog_config_dir = r'/etc/systemd/system.conf.d'
31+
watchdog_config_file = r'/etc/systemd/system.conf.d/watchdog.conf'
32+
modules_load_file = r'/etc/modules-load.d/vyos-watchdog.conf'
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 not watchdog:
54+
return None
55+
56+
# Check if watchdog is enabled
57+
if 'enable' not in watchdog:
58+
# Allow setting only the module without enabling watchdog
59+
allowed_keys = {'module'}
60+
extra_keys = set(watchdog.keys()) - allowed_keys
61+
if extra_keys:
62+
raise ConfigError(
63+
'Watchdog must be enabled to configure timeout values!\n'
64+
'Use "set system watchdog enable" to enable the watchdog.'
65+
)
66+
67+
return None
68+
69+
70+
def generate(watchdog):
71+
# If watchdog node removed entirely, clean up everything
72+
if not watchdog:
73+
if os.path.exists(watchdog_config_file):
74+
os.unlink(watchdog_config_file)
75+
if os.path.exists(modules_load_file):
76+
os.unlink(modules_load_file)
77+
return None
78+
79+
# Persist kernel module autoload on boot if specified (even if not enabled)
80+
module = watchdog.get('module')
81+
if module:
82+
try:
83+
write_file(modules_load_file, f"{module}\n")
84+
except Exception as e:
85+
print(f"Warning: Failed writing modules-load configuration: {e}")
86+
else:
87+
# If module option removed, drop persisted autoload file
88+
if os.path.exists(modules_load_file):
89+
os.unlink(modules_load_file)
90+
91+
# If not enabled, ensure systemd watchdog config is absent and return
92+
if 'enable' not in watchdog:
93+
if os.path.exists(watchdog_config_file):
94+
os.unlink(watchdog_config_file)
95+
return None
96+
97+
# Try to load kernel module if specified and /dev/watchdog0 is missing
98+
if not os.path.exists('/dev/watchdog0'):
99+
if module:
100+
# Try to load the module using vyos cmd wrapper for logging/airbag integration
101+
try:
102+
cmd(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 os.path.exists('/dev/watchdog0'):
107+
print(
108+
"Warning: /dev/watchdog0 not found. Systemd watchdog will not be enabled."
109+
)
110+
if os.path.exists(watchdog_config_file):
111+
os.unlink(watchdog_config_file)
112+
return None
113+
114+
# Ensure the directory exists
115+
os.makedirs(watchdog_config_dir, exist_ok=True)
116+
117+
# Pass through configured time values directly (systemd accepts raw seconds or unit-suffixed values)
118+
119+
render(watchdog_config_file, 'system/watchdog.conf.j2', watchdog)
120+
121+
return None
122+
123+
124+
def apply(watchdog):
125+
# Reload systemd daemon to apply watchdog configuration
126+
# The watchdog settings take effect after systemd is reloaded
127+
cmd('systemctl daemon-reload')
128+
129+
return None
130+
131+
132+
if __name__ == '__main__':
133+
try:
134+
c = get_config()
135+
verify(c)
136+
generate(c)
137+
apply(c)
138+
except ConfigError as e:
139+
print(e)
140+
exit(1)

0 commit comments

Comments
 (0)