From c1f020f98643ae2a521f7044874cf404bea40ad0 Mon Sep 17 00:00:00 2001 From: Xiaoxue Wang Date: Wed, 8 Jan 2025 18:02:45 +0800 Subject: [PATCH] feat: add spec parser and combiner for grubby_info Signed-off-by: Xiaoxue Wang --- docs/shared_combiners_catalog/grubby.rst | 3 + insights/combiners/grubby.py | 38 ++++++++ insights/parsers/grubby.py | 92 ++++++++++++++++++ insights/specs/__init__.py | 1 + insights/specs/default.py | 1 + insights/tests/combiners/test_grubby.py | 67 +++++++++++++ insights/tests/parsers/test_grubby.py | 116 ++++++++++++++++++++++- 7 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 docs/shared_combiners_catalog/grubby.rst create mode 100644 insights/combiners/grubby.py create mode 100644 insights/tests/combiners/test_grubby.py diff --git a/docs/shared_combiners_catalog/grubby.rst b/docs/shared_combiners_catalog/grubby.rst new file mode 100644 index 0000000000..3e1d6a79a2 --- /dev/null +++ b/docs/shared_combiners_catalog/grubby.rst @@ -0,0 +1,3 @@ +.. automodule:: insights.combiners.grubby + :members: + :show-inheritance: diff --git a/insights/combiners/grubby.py b/insights/combiners/grubby.py new file mode 100644 index 0000000000..72733d3843 --- /dev/null +++ b/insights/combiners/grubby.py @@ -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] diff --git a/insights/parsers/grubby.py b/insights/parsers/grubby.py index 971286e8a4..3c08205b01 100644 --- a/insights/parsers/grubby.py +++ b/insights/parsers/grubby.py @@ -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 @@ -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") diff --git a/insights/specs/__init__.py b/insights/specs/__init__.py index 8ada6a254c..665bc14030 100644 --- a/insights/specs/__init__.py +++ b/insights/specs/__init__.py @@ -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() diff --git a/insights/specs/default.py b/insights/specs/default.py index 17e25d9f13..30dc2e1722 100644 --- a/insights/specs/default.py +++ b/insights/specs/default.py @@ -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" diff --git a/insights/tests/combiners/test_grubby.py b/insights/tests/combiners/test_grubby.py new file mode 100644 index 0000000000..c2fba64d17 --- /dev/null +++ b/insights/tests/combiners/test_grubby.py @@ -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) diff --git a/insights/tests/parsers/test_grubby.py b/insights/tests/parsers/test_grubby.py index fc2babe0b5..014fd13ec4 100644 --- a/insights/tests/parsers/test_grubby.py +++ b/insights/tests/parsers/test_grubby.py @@ -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' @@ -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)) @@ -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