grub_conf resource: fix menuentry detection (#2408)

* Fix `grub_conf` menuentry detection

This does the following:
  - Corrects Grub2 bug where last entry was always selected
  - Adds support for specifying a Grub2 menu entry by name
  - Adds support for using `GRUB_DEFAULT=saved` with Grub2
  - Adds more Unit tests

* Add error if menuentry name cannot be extracted
* Add handling for missing/unreadable grubenv
* Add defensive code for failed menuentry extraction
This commit is contained in:
Jerry Aldrich 2018-01-18 13:20:48 -08:00 committed by Dominik Richter
parent 2cfc0acaae
commit 944dfdc987
8 changed files with 329 additions and 37 deletions

View file

@ -36,6 +36,7 @@ class GrubConfig < Inspec.resource(1)
elsif os.debian?
@conf_path = path || '/boot/grub/grub.cfg'
@defaults_path = '/etc/default/grub'
@grubenv_path = '/boot/grub2/grubenv'
@version = 'grub2'
elsif os[:name] == 'amazon'
@conf_path = path || '/etc/grub.conf'
@ -52,6 +53,7 @@ class GrubConfig < Inspec.resource(1)
else
@conf_path = path || '/boot/grub2/grub.cfg'
@defaults_path = '/etc/default/grub'
@grubenv_path = '/boot/grub2/grubenv'
@version = 'grub2'
end
end
@ -71,35 +73,73 @@ class GrubConfig < Inspec.resource(1)
######################################################################
def grub2_parse_kernel_lines(content, conf)
# Find all "menuentry" lines and then parse them into arrays
menu_entry = 0
menu_entries = extract_menu_entries(content)
if @kernel == 'default'
default_menu_entry(menu_entries, conf['GRUB_DEFAULT'])
else
menu_entries.find { |entry| entry['name'] == @kernel }
end
end
def extract_menu_entries(content)
menu_entries = []
lines = content.split("\n")
kernel_opts = {}
kernel_opts['insmod'] = []
lines.each_with_index do |file_line, index|
next unless file_line =~ /(^|\s)menuentry\s.*/
lines.drop(index+1).each do |kernel_line|
next if kernel_line =~ /(^|\s)(menu|}).*/
if menu_entry == conf['GRUB_DEFAULT'].to_i && @kernel == 'default'
if kernel_line =~ /(^|\s)initrd.*/
kernel_opts['initrd'] = kernel_line.split(' ')[1]
end
if kernel_line =~ /(^|\s)linux.*/
kernel_opts['kernel'] = kernel_line.split
end
if kernel_line =~ /(^|\s)set root=.*/
kernel_opts['root'] = kernel_line.split('=')[1].tr('\'', '')
end
if kernel_line =~ /(^|\s)insmod.*/
kernel_opts['insmod'].push(kernel_line.split(' ')[1])
end
else
menu_entry += 1
break
lines.each_with_index do |line, index|
next unless line =~ /^menuentry\s+.*/
entry = {}
entry['insmod'] = []
# Extract name from menuentry line
capture_data = line.match(/(?:^|\s+).*menuentry\s*['|"](.*)['|"]\s*--/)
if capture_data.nil? || capture_data.captures[0].nil?
raise Inspec::Exceptions::ResourceFailed "Failed to extract menuentry name from #{line}"
end
entry['name'] = capture_data.captures[0]
# Begin processing from index forward until a `}` line is met
lines.drop(index+1).each do |mline|
break if mline =~ /^\s*}\s*$/
case mline
when /(?:^|\s*)initrd.*/
entry['initrd'] = mline.split(' ')[1]
when /(?:^|\s*)linux.*/
entry['kernel'] = mline.split
when /(?:^|\s*)set root=.*/
entry['root'] = mline.split('=')[1].tr('\'', '')
when /(?:^|\s*)insmod.*/
entry['insmod'] << mline.split(' ')[1]
end
end
menu_entries << entry
end
kernel_opts
menu_entries
end
def default_menu_entry(menu_entries, default)
# If the default entry isn't `saved` then a number is used as an index.
# By default this is `0`, which would be the first item in the list.
return menu_entries[default.to_i] unless default == 'saved'
grubenv_contents = inspec.file(@grubenv_path).content
# The location of the grubenv file is not guaranteed. In the case that
# the file does not exist this will return the 0th entry. This will also
# return the 0th entry if InSpec lacks permission to read the file. Both
# of these reflect the default Grub2 behavior.
return menu_entries[0] if grubenv_contents.nil?
default_name = SimpleConfig.new(grubenv_contents).params['saved_entry']
default_entry = menu_entries.select { |k| k['name'] == default_name }[0]
return default_entry unless default_entry.nil?
# It is possible for the saved entry to not be valid . For example, grubenv
# not being up to date. If so, the 0th entry is the default.
menu_entries[0]
end
###################################################################

View file

@ -125,6 +125,11 @@ class MockLoader
'/etc/inetd.conf' => mockfile.call('inetd.conf'),
'/etc/group' => mockfile.call('etcgroup'),
'/etc/grub.conf' => mockfile.call('grub.conf'),
'/boot/grub2/grub.cfg' => mockfile.call('grub2.cfg'),
'/boot/grub2/grubenv' => mockfile.call('grubenv'),
'/boot/grub2/grubenv_invalid' => mockfile.call('grubenv_invalid'),
'/etc/default/grub' => mockfile.call('grub_defaults'),
'/etc/default/grub_with_saved' => mockfile.call('grub_defaults_with_saved'),
'/etc/audit/auditd.conf' => mockfile.call('auditd.conf'),
'/etc/mysql/my.cnf' => mockfile.call('mysql.conf'),
'/etc/mysql/mysql2.conf' => mockfile.call('mysql2.conf'),

View file

@ -0,0 +1,144 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically generated by grub2-mkconfig using templates
# from /etc/grub.d and settings from /etc/default/grub
#
### BEGIN /etc/grub.d/00_header ###
set pager=1
if [ -s $prefix/grubenv ]; then
load_env
fi
if [ "${next_entry}" ] ; then
set default="${next_entry}"
set next_entry=
save_env next_entry
set boot_once=true
else
set default="${saved_entry}"
fi
if [ x"${feature_menuentry_id}" = xy ]; then
menuentry_id_option="--id"
else
menuentry_id_option=""
fi
export menuentry_id_option
if [ "${prev_saved_entry}" ]; then
set saved_entry="${prev_saved_entry}"
save_env saved_entry
set prev_saved_entry=
save_env prev_saved_entry
set boot_once=true
fi
function savedefault {
if [ -z "${boot_once}" ]; then
saved_entry="${chosen}"
save_env saved_entry
fi
}
function load_video {
if [ x$feature_all_video_module = xy ]; then
insmod all_video
else
insmod efi_gop
insmod efi_uga
insmod ieee1275_fb
insmod vbe
insmod vga
insmod video_bochs
insmod video_cirrus
fi
}
terminal_output console
if [ x$feature_timeout_style = xy ] ; then
set timeout_style=menu
set timeout=5
# Fallback normal timeout code in case the timeout_style feature is
# unavailable.
else
set timeout=5
fi
### END /etc/grub.d/00_header ###
### BEGIN /etc/grub.d/00_tuned ###
set tuned_params=""
### END /etc/grub.d/00_tuned ###
### BEGIN /etc/grub.d/10_linux ###
menuentry 'Test GRUB_DEFAULT of 0' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-229.el7.x86_64-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_msdos
insmod xfs
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f
else
search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f
fi
linux16 /vmlinuz-yup-kernel-works root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet LANG=en_US.UTF-8
initrd16 /initramfs-yup-initrd-works
}
menuentry 'CentOS Linux 7 (Core), with Linux 3.10.0-229.el7.x86_64' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-229.el7.x86_64-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_msdos
insmod xfs
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f
else
search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f
fi
linux16 /vmlinuz-3.10.0-229.el7.x86_64 root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet LANG=en_US.UTF-8
initrd16 /initramfs-3.10.0-229.el7.x86_64.img
}
menuentry 'CentOS Linux 7 (Core), with Linux 0-rescue' --class rhel fedora --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-0-rescue-02ab91023f244febb8c38be42e37cc2d-advanced-c0bb4384-69a4-4aff-93ea-7ec2ea8e3a63' {
load_video
insmod gzio
insmod part_msdos
insmod xfs
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1' 57ba6944-e05b-4f02-95d4-ea9505fe584f
else
search --no-floppy --fs-uuid --set=root 57ba6944-e05b-4f02-95d4-ea9505fe584f
fi
linux16 /vmlinuz-0-rescue root=/dev/mapper/centos-root ro rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet
initrd16 /initramfs-0-rescue.img
}
### END /etc/grub.d/10_linux ###
### BEGIN /etc/grub.d/20_linux_xen ###
### END /etc/grub.d/20_linux_xen ###
### BEGIN /etc/grub.d/20_ppc_terminfo ###
### END /etc/grub.d/20_ppc_terminfo ###
### BEGIN /etc/grub.d/30_os-prober ###
### END /etc/grub.d/30_os-prober ###
### BEGIN /etc/grub.d/40_custom ###
# This file provides an easy way to add custom menu entries. Simply type the
# menu entries you want to add after this comment. Be careful not to change
# the 'exec tail' line above.
### END /etc/grub.d/40_custom ###
### BEGIN /etc/grub.d/41_custom ###
if [ -f ${config_directory}/custom.cfg ]; then
source ${config_directory}/custom.cfg
elif [ -z "${config_directory}" -a -f $prefix/custom.cfg ]; then
source $prefix/custom.cfg;
fi
### END /etc/grub.d/41_custom ###

View file

@ -0,0 +1,34 @@
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
# info -f grub -n 'Simple configuration'
GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX=""
# Uncomment to enable BadRAM filtering, modify to suit your needs
# This works with Linux (no patch required) and with any kernel that obtains
# the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...)
#GRUB_BADRAM="0x01234567,0xfefefefe,0x89abcdef,0xefefefef"
# Uncomment to disable graphical terminal (grub-pc only)
#GRUB_TERMINAL=console
# The resolution used on graphical terminal
# note that you can use only modes which your graphic card supports via VBE
# you can see them in real GRUB with the command `vbeinfo'
#GRUB_GFXMODE=640x480
# Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux
#GRUB_DISABLE_LINUX_UUID=true
# Uncomment to disable generation of recovery mode menu entries
#GRUB_DISABLE_RECOVERY="true"
# Uncomment to get a beep at grub start
#GRUB_INIT_TUNE="480 440 1"

View file

@ -0,0 +1,6 @@
GRUB_TIMEOUT=5
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/root rd.lvm.lv=centos/swap crashkernel=auto rhgb quiet"
GRUB_DISABLE_RECOVERY="true"

View file

@ -0,0 +1,3 @@
# GRUB Environment Block
saved_entry=CentOS Linux 7 (Core), with Linux 3.10.0-229.el7.x86_64
############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################

View file

@ -0,0 +1,3 @@
# GRUB Environment Block
saved_entry=Nope, not a real entry
############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################

View file

@ -6,24 +6,81 @@ require 'inspec/resource'
describe 'Inspec::Resources::GrubConfig' do
it 'verify kernel include' do
resource = MockLoader.new(:centos6).load_resource('grub_conf')
_(resource.kernel).must_be_kind_of Array
# Grub2 with `GRUB_DEFAULT=0`
it 'parses correctly with grub2 and `GRUB_DEfAULT=0`' do
resource = MockLoader.new(:centos7).load_resource('grub_conf')
resource.kernel.must_include '/vmlinuz-yup-kernel-works'
resource.initrd.must_equal '/initramfs-yup-initrd-works'
end
it 'verify initrd include' do
resource = MockLoader.new(:centos6).load_resource('grub_conf')
_(resource.initrd).must_be_kind_of String
# Grub2 with `GRUB_DEFAULT=saved`
it 'parses correctly with grub2 and `saved` as the `GRUB_DEFAULT`' do
resource = MockLoader.new(:centos7).load_resource('grub_conf')
# Both Grub1 and Grub2 use `/etc/default/grub`.
# This overrides the Grub1 default for testing.
resource.instance_variable_set(
:@defaults_path,
'/etc/default/grub_with_saved'
)
resource.kernel.must_include '/vmlinuz-3.10.0-229.el7.x86_64'
resource.initrd.must_equal '/initramfs-3.10.0-229.el7.x86_64.img'
end
it 'verify default' do
resource = MockLoader.new(:centos6).load_resource('grub_conf')
_(resource.default).must_equal '0'
it 'parses correctly with grub2 and an invalid grubenv entry' do
resource = MockLoader.new(:centos7).load_resource('grub_conf')
# Both Grub1 and Grub2 use `/etc/default/grub`.
# This overrides the Grub1 default for testing.
resource.instance_variable_set(
:@defaults_path,
'/etc/default/grub_with_saved'
)
resource.instance_variable_set(
:@grubenv_path,
'/boot/grub2/grubenv_invalid'
)
resource.kernel.must_include '/vmlinuz-yup-kernel-works'
resource.initrd.must_equal '/initramfs-yup-initrd-works'
end
it 'verify timeout' do
resource = MockLoader.new(:centos6).load_resource('grub_conf')
_(resource.timeout).must_equal '5'
# Grub2 with a specified kernel
it 'parses data correctly with grub2 and a specified kernel' do
resource = MockLoader.new(:centos7).load_resource(
'grub_conf',
'/boot/grub2/grub.cfg',
'CentOS Linux 7 (Core), with Linux 0-rescue'
)
resource.kernel.must_include '/vmlinuz-0-rescue'
resource.initrd.must_equal '/initramfs-0-rescue.img'
end
# Legacy Grub
it 'parses correctly with grub1 (aka legacy-grub)' do
resource = MockLoader.new(:centos6).load_resource('grub_conf')
resource.kernel.must_include '/vmlinuz-2.6.32-573.7.1.el6.x86_64'
resource.initrd.must_equal '/initramfs-2.6.32-573.7.1.el6.x86_64.img'
resource.default.must_equal '0'
resource.timeout.must_equal '5'
end
# Legacy Grub with a specified kernel
it 'parses data correctly with grub1 and a specified kernel' do
resource = MockLoader.new(:centos6).load_resource(
'grub_conf',
'/etc/grub.conf',
'CentOS 6 (2.6.32-573.el6.x86_64)'
)
resource.kernel.must_include '/vmlinuz-2.6.32-573.el6.x86_64'
resource.initrd.must_equal '/initramfs-2.6.32-573.el6.x86_64.img'
resource.default.must_equal '0'
resource.timeout.must_equal '5'
end
end