Add clean step 'erase_pstore'

Add an automatic clean step to clean the Linux kernel's pstore.
The step is disabled by default.

Story: #2008317
Task: #41214

Change-Id: Ie1a42dfff4c7e1c7abeaf39feca956bb9e2ea497
This commit is contained in:
Arne Wiebalck 2020-11-11 11:22:31 +01:00
parent 1f590ea382
commit 92e26b01e9
4 changed files with 117 additions and 0 deletions

View File

@ -35,6 +35,9 @@ Clean steps
``deploy.erase_devices_metadata``
Erases partition tables from all recognized disk devices. Can be used as
an alternative to the much longer ``erase_devices`` step.
``deploy.erase_pstore``
Erases entries from pstore, the kernel's oops/panic logger. Disabled by
default. Can be enabled via priority overrides.
``raid.create_configuration``
Create a RAID configuration. This step belongs to the ``raid`` interface
and must be used through the :ironic-doc:`ironic RAID feature

View File

@ -22,6 +22,7 @@ from multiprocessing.pool import ThreadPool
import os
import re
import shlex
import shutil
import time
from ironic_lib import disk_utils
@ -1314,6 +1315,37 @@ class GenericHardwareManager(HardwareManager):
for k, v in erase_errors.items()]))
raise errors.BlockDeviceEraseError(excpt_msg)
def _find_pstore_mount_point(self):
"""Find the pstore mount point by scanning /proc/mounts.
:returns: The pstore mount if existing, none otherwise.
"""
with open("/proc/mounts", "r") as mounts:
for line in mounts:
# /proc/mounts format is: "device mountpoint fstype ..."
m = re.match(r'^pstore (\S+) pstore', line)
if m:
return m.group(1)
def erase_pstore(self, node, ports):
"""Attempt to erase the kernel pstore.
:param node: Ironic node object
:param ports: list of Ironic port objects
"""
pstore_path = self._find_pstore_mount_point()
if not pstore_path:
LOG.debug("No pstore found")
return
LOG.info("Cleaning up pstore in %s", pstore_path)
for file in os.listdir(pstore_path):
filepath = os.path.join(pstore_path, file)
try:
shutil.rmtree(filepath)
except OSError:
os.remove(filepath)
def _shred_block_device(self, node, block_device):
"""Erase a block device using shred.
@ -1680,6 +1712,13 @@ class GenericHardwareManager(HardwareManager):
'reboot_requested': False,
'abortable': True
},
{
'step': 'erase_pstore',
'priority': 0,
'interface': 'deploy',
'reboot_requested': False,
'abortable': True
},
{
'step': 'delete_configuration',
'priority': 0,

View File

@ -14,6 +14,7 @@
import binascii
import os
import shutil
import time
from unittest import mock
@ -859,6 +860,24 @@ MDADM_EXAMINE_OUTPUT_NON_MEMBER = ("""/dev/sdz1:
""")
PROC_MOUNTS_OUTPUT = ("""
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
/dev/sda2 / ext4 rw,relatime,errors=remount-ro 0 0
tmpfs /run/user/1000 tmpfs rw,nosuid,nodev,relatime 0 0
pstore /sys/fs/pstore pstore rw,nosuid,nodev,noexec,relatime 0 0
/dev/loop19 /snap/core/10126 squashfs ro,nodev,relatime 0 0
""")
PROC_MOUNTS_OUTPUT_NO_PSTORE = ("""
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
/dev/sda2 / ext4 rw,relatime,errors=remount-ro 0 0
tmpfs /run/user/1000 tmpfs rw,nosuid,nodev,relatime 0 0
pstore /sys/fs/pstore qstore rw,nosuid,nodev,noexec,relatime 0 0
/dev/loop19 /snap/core/10126 squashfs ro,nodev,relatime 0 0
""")
class FakeHardwareManager(hardware.GenericHardwareManager):
def __init__(self, hardware_support):
self._hardware_support = hardware_support
@ -921,6 +940,13 @@ class TestGenericHardwareManager(base.IronicAgentTest):
'reboot_requested': False,
'abortable': True
},
{
'step': 'erase_pstore',
'priority': 0,
'interface': 'deploy',
'reboot_requested': False,
'abortable': True
},
{
'step': 'delete_configuration',
'priority': 0,
@ -2631,6 +2657,44 @@ class TestGenericHardwareManager(base.IronicAgentTest):
test_security_erase_option(
self, False, '--security-erase')
def test__find_pstore_mount_point(self):
with mock.patch('builtins.open',
mock.mock_open(),
create=True) as mocked_open:
mocked_open.return_value.__iter__ = \
lambda self: iter(PROC_MOUNTS_OUTPUT.splitlines())
self.assertEqual(self.hardware._find_pstore_mount_point(),
"/sys/fs/pstore")
mocked_open.assert_called_once_with('/proc/mounts', 'r')
def test__find_pstore_mount_point_no_pstore(self):
with mock.patch('builtins.open',
mock.mock_open(),
create=True) as mocked_open:
mocked_open.return_value.__iter__.return_value = \
PROC_MOUNTS_OUTPUT_NO_PSTORE.splitlines()
self.assertIsNone(self.hardware._find_pstore_mount_point())
mocked_open.assert_called_once_with('/proc/mounts', 'r')
@mock.patch('os.listdir', autospec=True)
@mock.patch.object(shutil, 'rmtree', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
'_find_pstore_mount_point', autospec=True)
def test_erase_pstore(self, mocked_find_pstore, mocked_rmtree,
mocked_listdir):
mocked_find_pstore.return_value = '/sys/fs/pstore'
pstore_entries = ['dmesg-erst-663482778',
'dmesg-erst-663482779']
mocked_listdir.return_value = pstore_entries
self.hardware.erase_pstore(self.node, [])
mocked_listdir.assert_called_once()
self.assertEqual(mocked_rmtree.call_count,
len(pstore_entries))
mocked_rmtree.assert_has_calls([
mock.call('/sys/fs/pstore/' + arg) for arg in pstore_entries
])
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager,
'_is_virtual_media_device', autospec=True)

View File

@ -0,0 +1,11 @@
---
features:
- |
Adds a clean step to erase the Linux kernel's pstore. The step is disabled
by default.
security:
- |
If enabled, the new clean step 'erase_pstore' removes all pstore entries
(the oops/panic logs from a failing kernel) upon cleaning. This is to
reduce the risk that potentially sensitive data is preserved across
instantiations (and therefore different users) of a bare metal node.