Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add spec parser and combiner for grubby_info #4329

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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
Copy link
Contributor

Choose a reason for hiding this comment

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

As a combiner, it would be good to combine more useful attribute here, e.g. devs can use the combiner rather than access each single parser to fetch particular info.


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
Loading