Skip to content

Commit

Permalink
feat: add spec parser and combiner for grubby_info
Browse files Browse the repository at this point in the history
Signed-off-by: Xiaoxue Wang <[email protected]>
  • Loading branch information
JoySnow committed Jan 8, 2025
1 parent 9c64871 commit c1f020f
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 1 deletion.
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:
38 changes: 38 additions & 0 deletions insights/combiners/grubby.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
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 current default boot entry
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

@property
def default_boot_entry(self):
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())))
return self.boot_entries[self.default_index]
92 changes: 92 additions & 0 deletions insights/parsers/grubby.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
GrubbyDefaultKernel - command ``grubby --default-kernel``
---------------------------------------------------------
GrubbyInfoAll - command ``grubby --info=ALL``
---------------------------------------------
"""
from insights.core import CommandParser
from insights.core.exceptions import ParseException, SkipComponent
Expand Down Expand Up @@ -91,3 +94,92 @@ 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 @@ -378,6 +378,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
67 changes: 67 additions & 0 deletions insights/tests/combiners/test_grubby.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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()


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 len(result.boot_entries) == 3

grubby_info_all = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL))
grubby_default_index = GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_2))
result = Grubby(grubby_info_all, grubby_default_index)

assert result.default_index == 3
assert len(result.boot_entries) == 3

with pytest.raises(ParseException) as excinfo:
result.default_boot_entry
assert "DEFAULT index 3 not exist in parsed boot_entries: [0, 1, 2]" in str(excinfo.value)
116 changes: 115 additions & 1 deletion insights/tests/parsers/test_grubby.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

from insights.core.exceptions import ParseException, SkipComponent
from insights.parsers import grubby
from insights.parsers.grubby import GrubbyDefaultIndex, GrubbyDefaultKernel
from insights.parsers.grubby import (
GrubbyDefaultIndex,
GrubbyDefaultKernel,
GrubbyInfoAll
)
from insights.tests import context_wrap

DEFAULT_INDEX_1 = '0'
Expand Down Expand Up @@ -45,6 +49,67 @@
/boot/vmlinuz-4.18.0-425.10.1.el8_7.x86_64
""".strip()

GRUBBY_INFO_ALL_1 = """
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"
""".strip()

GRUBBY_INFO_ALL_2 = """
index=0
kernel=/boot/vmlinuz-3.10.0-862.el7.x86_64
args="ro console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto LANG=en_US.UTF-8"
root=UUID=6bea2b7b-e6cc-4dba-ac79-be6530d348f5
initrd=/boot/initramfs-3.10.0-862.el7.x86_64.img
title=Red Hat Enterprise Linux Server (3.10.0-862.el7.x86_64) 7.5 (Maipo)
index=1
kernel=/boot/vmlinuz-0-rescue-1b461b2e96854984bc0777c4b4b518a9
args="ro console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto"
root=UUID=6bea2b7b-e6cc-4dba-ac79-be6530d348f5
initrd=/boot/initramfs-0-rescue-1b461b2e96854984bc0777c4b4b518a9.img
title=Red Hat Enterprise Linux Server (0-rescue-1b461b2e96854984bc0777c4b4b518a9) 7.5 (Maipo)
index=2
non linux entry
""".strip()

GRUBBY_INFO_ALL_INVALID_1 = """
some head lines
index=0
non linux entry
some tail lines
""".strip()
GRUBBY_INFO_ALL_INVALID_2 = """
some head lines
kernel=/boot/vmlinuz-3.10.0-862.el7.x86_64
args="ro console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto LANG=en_US.UTF-8"
root=UUID=6bea2b7b-e6cc-4dba-ac79-be6530d348f5
initrd=/boot/initramfs-3.10.0-862.el7.x86_64.img
title=Red Hat Enterprise Linux Server (3.10.0-862.el7.x86_64) 7.5 (Maipo)
some tail lines
""".strip()
GRUBBY_INFO_ALL_INVALID_2 = """
some head lines
index=some-index
kernel=/boot/vmlinuz-3.10.0-862.el7.x86_64
args="ro console=tty0 console=ttyS0,115200n8 no_timer_check net.ifnames=0 crashkernel=auto LANG=en_US.UTF-8"
root=UUID=6bea2b7b-e6cc-4dba-ac79-be6530d348f5
initrd=/boot/initramfs-3.10.0-862.el7.x86_64.img
title=Red Hat Enterprise Linux Server (3.10.0-862.el7.x86_64) 7.5 (Maipo)
some tail lines
""".strip()


def test_grubby_default_index():
res = GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_1))
Expand Down Expand Up @@ -101,10 +166,59 @@ def test_grubby_default_kernel_ab():
assert 'Invalid output:' in str(excinfo.value)


def test_grubby_info_all():
res = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_1))

assert res.unparsed_lines == []
assert len(res.boot_entries) == 2
assert res.boot_entries[0] == 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 "kernel" in res.boot_entries[1]
assert res.boot_entries[1]["kernel"] == "/boot/vmlinuz-5.14.0-70.13.1.el9_0.x86_64"
assert res.boot_entries[1].get("root") == "/dev/mapper/rhel-root"

entry_args = res.boot_entries[1].get("args")
assert entry_args.get("ro") == [True]
assert entry_args.get("rd.lvm.lv") == ['rhel/root', 'rhel/swap']

res = GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_2))
assert len(res.boot_entries) == 2
assert res.unparsed_lines == ["non linux entry"]


def test_grubby_info_all_ab():
with pytest.raises(SkipComponent) as excinfo:
GrubbyInfoAll(context_wrap(""))
assert 'Empty output' in str(excinfo.value)

with pytest.raises(SkipComponent) as excinfo:
GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_INVALID_1))
assert 'No valid entry parsed' in str(excinfo.value)

with pytest.raises(ParseException) as excinfo:
GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_INVALID_2))
assert 'Invalid index value:' in str(excinfo.value)


def test_doc_examples():
env = {
'grubby_default_index': GrubbyDefaultIndex(context_wrap(DEFAULT_INDEX_1)),
'grubby_default_kernel': GrubbyDefaultKernel(context_wrap(DEFAULT_KERNEL)),
'grubby_info_all': GrubbyInfoAll(context_wrap(GRUBBY_INFO_ALL_1)),
}
failed, total = doctest.testmod(grubby, globs=env)
assert failed == 0

0 comments on commit c1f020f

Please sign in to comment.