Skip to content

Commit 2720e19

Browse files
Copilotsilug
andauthored
Fix runlevel facter for distros without /sbin/runlevel binary
- Use Facter::Core::Execution.which to check for runlevel binary - Fall back to systemctl get-default with target→runlevel mapping - Use Facter::Core::Execution.exec instead of backticks - Add unit tests covering all scenarios Agent-Logs-Url: https://github.com/simp/pupmod-simp-simplib/sessions/7a97386a-2830-4d7b-b32a-861a349ec7bd Co-authored-by: silug <206992+silug@users.noreply.github.com>
1 parent dc29c5c commit 2720e19

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

lib/facter/runlevel.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
Facter.add('runlevel') do
77
confine kernel: 'Linux'
88
setcode do
9-
`"/sbin/runlevel"`.split.last
9+
runlevel = nil
10+
11+
if (runlevel_path = Facter::Core::Execution.which('runlevel'))
12+
result = Facter::Core::Execution.exec(runlevel_path)
13+
runlevel = result.split.last if result
14+
elsif Facter::Core::Execution.which('systemctl')
15+
# Map systemd targets to traditional SysV runlevels
16+
target_to_runlevel = {
17+
'poweroff.target' => '0',
18+
'rescue.target' => '1',
19+
'multi-user.target' => '3',
20+
'graphical.target' => '5',
21+
'reboot.target' => '6',
22+
}
23+
result = Facter::Core::Execution.exec('systemctl get-default')
24+
runlevel = target_to_runlevel[result&.strip]
25+
end
26+
27+
runlevel
1028
end
1129
end

spec/unit/facter/runlevel_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'runlevel' do
6+
before :each do
7+
Facter.clear
8+
9+
# mock out Facter method called when evaluating confine for :kernel
10+
allow(Facter::Core::Execution).to receive(:exec).with('uname -s').and_return('Linux')
11+
end
12+
13+
context 'when runlevel binary is available' do
14+
it 'returns the current runlevel' do
15+
expect(Facter::Core::Execution).to receive(:which).with('runlevel').and_return('/sbin/runlevel')
16+
expect(Facter::Core::Execution).to receive(:exec).with('/sbin/runlevel').and_return('N 3')
17+
18+
expect(Facter.fact('runlevel').value).to eq('3')
19+
end
20+
end
21+
22+
context 'when runlevel binary is not available' do
23+
before :each do
24+
expect(Facter::Core::Execution).to receive(:which).with('runlevel').and_return(nil)
25+
end
26+
27+
context 'when systemctl is available' do
28+
before :each do
29+
expect(Facter::Core::Execution).to receive(:which).with('systemctl').and_return('/usr/bin/systemctl')
30+
end
31+
32+
{
33+
'poweroff.target' => '0',
34+
'rescue.target' => '1',
35+
'multi-user.target' => '3',
36+
'graphical.target' => '5',
37+
'reboot.target' => '6',
38+
}.each do |target, expected_runlevel|
39+
context "when the default target is #{target}" do
40+
it "returns runlevel #{expected_runlevel}" do
41+
expect(Facter::Core::Execution).to receive(:exec).with('systemctl get-default').and_return("#{target}\n")
42+
43+
expect(Facter.fact('runlevel').value).to eq(expected_runlevel)
44+
end
45+
end
46+
end
47+
48+
context 'when the default target is an unmapped target' do
49+
it 'returns nil' do
50+
expect(Facter::Core::Execution).to receive(:exec).with('systemctl get-default').and_return("emergency.target\n")
51+
52+
expect(Facter.fact('runlevel').value).to be nil
53+
end
54+
end
55+
end
56+
57+
context 'when systemctl is not available' do
58+
it 'returns nil' do
59+
expect(Facter::Core::Execution).to receive(:which).with('systemctl').and_return(nil)
60+
61+
expect(Facter.fact('runlevel').value).to be nil
62+
end
63+
end
64+
end
65+
end

0 commit comments

Comments
 (0)