Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion lib/facter/runlevel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@
Facter.add('runlevel') do
confine kernel: 'Linux'
setcode do
`"/sbin/runlevel"`.split.last
runlevel = nil

if (runlevel_path = Facter::Core::Execution.which('runlevel'))
result = Facter::Core::Execution.exec(runlevel_path)
runlevel = result.split.last if result
elsif Facter::Core::Execution.which('systemctl')
# Map systemd targets to traditional SysV runlevels
target_to_runlevel = {
'poweroff.target' => '0',
'rescue.target' => '1',
'multi-user.target' => '3',
'graphical.target' => '5',
'reboot.target' => '6',
}
result = Facter::Core::Execution.exec('systemctl get-default')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemctl get-default will return the default boot target/"runlevel", but that's not necessarily its current state (which could include more than one runlevel-mapped target simultaneously; see "Q: How do I figure out the current runlevel?" at https://systemd.io/FAQ/ ).

A better way to approximate the number returned by runlevel in systemd would be to check the targets in target_to_runlevel from highest value to lowest, stopping at the first (highest-numbered) target that is currently active. So return 3 if multi-user.target is active and graphical.target is not. This is consistent with the behavior described in the runlevel man page.

runlevel = target_to_runlevel[result&.strip]
end

runlevel
end
end
65 changes: 65 additions & 0 deletions spec/unit/facter/runlevel_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

require 'spec_helper'

describe 'runlevel' do
before :each do
Facter.clear

# mock out Facter method called when evaluating confine for :kernel
allow(Facter::Core::Execution).to receive(:exec).with('uname -s').and_return('Linux')
end

context 'when runlevel binary is available' do
it 'returns the current runlevel' do
expect(Facter::Core::Execution).to receive(:which).with('runlevel').and_return('/sbin/runlevel')
expect(Facter::Core::Execution).to receive(:exec).with('/sbin/runlevel').and_return('N 3')

expect(Facter.fact('runlevel').value).to eq('3')
end
end

context 'when runlevel binary is not available' do
before :each do
expect(Facter::Core::Execution).to receive(:which).with('runlevel').and_return(nil)
end

context 'when systemctl is available' do
before :each do
expect(Facter::Core::Execution).to receive(:which).with('systemctl').and_return('/usr/bin/systemctl')
end

{
'poweroff.target' => '0',
'rescue.target' => '1',
'multi-user.target' => '3',
'graphical.target' => '5',
'reboot.target' => '6',
}.each do |target, expected_runlevel|
context "when the default target is #{target}" do
it "returns runlevel #{expected_runlevel}" do
expect(Facter::Core::Execution).to receive(:exec).with('systemctl get-default').and_return("#{target}\n")

expect(Facter.fact('runlevel').value).to eq(expected_runlevel)
end
end
end

context 'when the default target is an unmapped target' do
it 'returns nil' do
expect(Facter::Core::Execution).to receive(:exec).with('systemctl get-default').and_return("emergency.target\n")

expect(Facter.fact('runlevel').value).to be nil
end
end
end

context 'when systemctl is not available' do
it 'returns nil' do
expect(Facter::Core::Execution).to receive(:which).with('systemctl').and_return(nil)

expect(Facter.fact('runlevel').value).to be nil
end
end
end
end
Loading