Skip to content

Commit

Permalink
feat: add spec parser and combiner for grubby_info (#4329)
Browse files Browse the repository at this point in the history
Signed-off-by: Xiaoxue Wang <[email protected]>
  • Loading branch information
JoySnow authored Jan 15, 2025
1 parent dee4d55 commit bc73463
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 5 deletions.
3 changes: 3 additions & 0 deletions docs/shared_combiners_catalog/grubby.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.combiners.grubby
:members:
:show-inheritance:
46 changes: 46 additions & 0 deletions insights/combiners/grubby.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Grubby
======
Combiner for command ``/usr/sbin/grubby`` parsers.
This combiner uses the parsers:
:class:`insights.parsers.grubby.GrubbyDefaultIndex`,
:class:`insights.parsers.grubby.GrubbyInfoAll`.
"""

from insights.core.exceptions import ParseException
from insights.core.plugins import combiner
from insights.parsers.grubby import GrubbyDefaultIndex, GrubbyInfoAll


@combiner(GrubbyInfoAll, GrubbyDefaultIndex)
class Grubby(object):
"""
Combine command "grubby" parsers into one Combiner.
Attributes:
boot_entries (dict): All boot entries indexed by the entry "index"
default_index (int): The numeric index of the default boot entry
default_boot_entry (dict): The boot information for default kernel
default_kernel (str): The path of the default kernel
Raises:
ParseException: when parsing into error.
"""

def __init__(self, grubby_info_all, grubby_default_index):
self.boot_entries = grubby_info_all.boot_entries
self.default_index = grubby_default_index.default_index

if self.default_index not in self.boot_entries:
raise ParseException(
"DEFAULT index %s not exist in parsed boot_entries: %s"
% (self.default_index, list(self.boot_entries.keys()))
)
self.default_boot_entry = self.boot_entries[self.default_index]

self.default_kernel = self.default_boot_entry.get("kernel")
if not self.default_kernel:
raise ParseException(
"DEFAULT kernel-path not exist in default-index: %s" % self.default_index
)
111 changes: 110 additions & 1 deletion insights/parsers/grubby.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
grubby - command ``/usr/sbin/grubby``
Grubby - command ``/usr/sbin/grubby``
=====================================
This is a collection of parsers that all deal with the command ``grubby``.
Expand All @@ -10,11 +10,16 @@
GrubbyDefaultKernel - command ``grubby --default-kernel``
---------------------------------------------------------
GrubbyInfoAll - command ``grubby --info=ALL``
---------------------------------------------
"""

from insights.core import CommandParser
from insights.core.exceptions import ParseException, SkipComponent
from insights.core.plugins import parser
from insights.specs import Specs
from insights.util import deprecated


@parser(Specs.grubby_default_index)
Expand All @@ -37,6 +42,7 @@ class GrubbyDefaultIndex(CommandParser):
Attributes:
default_index (int): the numeric index of the current default boot entry, count from 0
"""

def parse_content(self, content):
if not content:
raise SkipComponent('Empty output')
Expand All @@ -48,6 +54,10 @@ def parse_content(self, content):
@parser(Specs.grubby_default_kernel)
class GrubbyDefaultKernel(CommandParser):
"""
.. warning::
This class is deprecated and will be removed from 3.7.0.
Please use the :class:`insights.combiners.grubby.Grubby` instead.
This parser parses the output of command ``grubby --default-kernel``.
The typical output of this command is::
Expand All @@ -66,6 +76,15 @@ class GrubbyDefaultKernel(CommandParser):
Attributes:
default_kernel(str): The default kernel name for next boot
"""

def __init__(self, context):
deprecated(
GrubbyDefaultKernel,
"Please use the :class:`insights.combiners.grubby.Grubby` instead.",
"3.7.0",
)
super(GrubbyDefaultKernel, self).__init__(context)

def parse_content(self, content):
if not content:
raise SkipComponent('Empty output')
Expand All @@ -91,3 +110,93 @@ def parse_content(self, content):
raise ParseException('Invalid output: unparsable kernel line: {0}', content)

self.default_kernel = default_kernel_str


@parser(Specs.grubby_info_all)
class GrubbyInfoAll(CommandParser):
"""
This parser parses the output of command ``grubby --info=ALL``.
Attributes:
boot_entries (dict): All boot entries indexed by the entry "index"
unparsed_lines (list): All the unparsable lines
The typical output of this command is::
index=0
kernel="/boot/vmlinuz-5.14.0-162.6.1.el9_1.x86_64"
args="ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet retbleed=stuff"
root="/dev/mapper/rhel-root"
initrd="/boot/initramfs-5.14.0-162.6.1.el9_1.x86_64.img"
title="Red Hat Enterprise Linux (5.14.0-162.6.1.el9_1.x86_64) 9.1 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-162.6.1.el9_1.x86_64"
index=1
kernel="/boot/vmlinuz-5.14.0-70.13.1.el9_0.x86_64"
args="ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet retbleed=stuff"
root="/dev/mapper/rhel-root"
initrd="/boot/initramfs-5.14.0-70.13.1.el9_0.x86_64.img"
title="Red Hat Enterprise Linux (5.14.0-70.13.1.el9_0.x86_64) 9.0 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-70.13.1.el9_0.x86_64"
Examples:
>>> len(grubby_info_all.boot_entries)
2
>>> grubby_info_all.boot_entries[0]["kernel"]
'/boot/vmlinuz-5.14.0-162.6.1.el9_1.x86_64'
>>> grubby_info_all.boot_entries[1].get("args").get("rd.lvm.lv")
['rhel/root', 'rhel/swap']
Raises:
SkipComponent: When output is empty
ParseException: When output is invalid
"""

def parse_content(self, content):

def _parse_args(args):
parsed_args = dict()
for el in args.split():
key, value = el, True
if "=" in el:
key, value = el.split("=", 1)
if key not in parsed_args:
parsed_args[key] = []
parsed_args[key].append(value)
return parsed_args

if not content:
raise SkipComponent("Empty output")

self.boot_entries = {}
self.unparsed_lines = []

entry_data = {}
for _line in content:
line = _line.strip()

if not line:
continue
if "=" not in line:
self.unparsed_lines.append(_line)
continue

k, v = line.split("=", 1)
v = v.strip("'\"")
if k == "index":
if v.isdigit():
if entry_data and "index" in entry_data and len(entry_data) > 1:
self.boot_entries[entry_data["index"]] = entry_data
entry_data = {k: int(v)}
else:
raise ParseException('Invalid index value: {0}', _line)
elif k == "args":
entry_data[k] = _parse_args(v)
else:
entry_data[k] = v

if entry_data and "index" in entry_data and len(entry_data) > 1:
self.boot_entries[entry_data["index"]] = entry_data

if not self.boot_entries:
raise SkipComponent("No valid entry parsed")
1 change: 1 addition & 0 deletions insights/specs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ class Specs(SpecSet):
grub_efi_conf = RegistryPoint()
grubby_default_index = RegistryPoint(no_obfuscate=['hostname', 'ip'])
grubby_default_kernel = RegistryPoint(no_obfuscate=['hostname', 'ip'])
grubby_info_all = RegistryPoint(no_obfuscate=['hostname', 'ip'])
grubenv = RegistryPoint()
hammer_ping = RegistryPoint()
hammer_task_list = RegistryPoint()
Expand Down
1 change: 1 addition & 0 deletions insights/specs/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ class DefaultSpecs(Specs):
"/usr/sbin/grubby --default-index"
) # only RHEL7 and updwards
grubby_default_kernel = simple_command("/sbin/grubby --default-kernel")
grubby_info_all = simple_command("/usr/sbin/grubby --info=ALL")
grub_conf = simple_file("/boot/grub/grub.conf")
grub_config_perms = simple_command(
"/bin/ls -lH /boot/grub2/grub.cfg"
Expand Down
79 changes: 79 additions & 0 deletions insights/tests/combiners/test_grubby.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from insights.combiners.grubby import Grubby
from insights.core.exceptions import ParseException
from insights.parsers.grubby import GrubbyInfoAll, GrubbyDefaultIndex
from insights.tests import context_wrap
import pytest

DEFAULT_INDEX_1 = '0'
DEFAULT_INDEX_2 = '3'

GRUBBY_INFO_ALL = """
index=0
kernel="/boot/vmlinuz-5.14.0-162.6.1.el9_1.x86_64"
args="ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet retbleed=stuff"
root="/dev/mapper/rhel-root"
initrd="/boot/initramfs-5.14.0-162.6.1.el9_1.x86_64.img"
title="Red Hat Enterprise Linux (5.14.0-162.6.1.el9_1.x86_64) 9.1 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-162.6.1.el9_1.x86_64"
index=1
kernel="/boot/vmlinuz-5.14.0-70.13.1.el9_0.x86_64"
args="ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet retbleed=stuff"
root="/dev/mapper/rhel-root"
initrd="/boot/initramfs-5.14.0-70.13.1.el9_0.x86_64.img"
title="Red Hat Enterprise Linux (5.14.0-70.13.1.el9_0.x86_64) 9.0 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-70.13.1.el9_0.x86_64"
index=2
kernel="/boot/vmlinuz-0-rescue-4d684a4a6166439a867e701ded4f7e10"
args="ro crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet retbleed=stuff"
root="/dev/mapper/rhel-root"
initrd="/boot/initramfs-0-rescue-4d684a4a6166439a867e701ded4f7e10.img"
title="Red Hat Enterprise Linux (0-rescue-4d684a4a6166439a867e701ded4f7e10) 9.0 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-0-rescue"
""".strip()

GRUBBY_INFO_ALL_AB_1 = """
index=0
title="Red Hat Enterprise Linux (5.14.0-162.6.1.el9_1.x86_64) 9.1 (Plow)"
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-162.6.1.el9_1.x86_64"
""".strip()


def test_grubby():
grubby_info_all = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL))
grubby_default_index = GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_1))
result = Grubby(grubby_info_all, grubby_default_index)

assert result.default_index == 0
assert result.default_boot_entry == dict(
index=0,
kernel="/boot/vmlinuz-5.14.0-162.6.1.el9_1.x86_64",
args={
'ro': [True],
'crashkernel': ['1G-4G:192M,4G-64G:256M,64G-:512M'],
'resume': ['/dev/mapper/rhel-swap'],
'rd.lvm.lv': ['rhel/root', 'rhel/swap'],
'rhgb': [True],
'quiet': [True],
'retbleed': ['stuff'],
},
root="/dev/mapper/rhel-root",
initrd="/boot/initramfs-5.14.0-162.6.1.el9_1.x86_64.img",
title="Red Hat Enterprise Linux (5.14.0-162.6.1.el9_1.x86_64) 9.1 (Plow)",
id="4d684a4a6166439a867e701ded4f7e10-5.14.0-162.6.1.el9_1.x86_64",
)
assert result.default_kernel == "/boot/vmlinuz-5.14.0-162.6.1.el9_1.x86_64"
assert len(result.boot_entries) == 3


def test_grubby_ab():
grubby_info_all = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL))
grubby_default_index = GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_2))
with pytest.raises(ParseException) as excinfo:
Grubby(grubby_info_all, grubby_default_index)
assert "DEFAULT index 3 not exist in parsed boot_entries: [0, 1, 2]" in str(excinfo.value)

grubby_info_all = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_AB_1))
grubby_default_index = GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_1))
with pytest.raises(ParseException) as excinfo:
Grubby(grubby_info_all, grubby_default_index)
assert "DEFAULT kernel-path not exist in default-index: 0" in str(excinfo.value)
Loading

0 comments on commit bc73463

Please sign in to comment.