Keep systemd-managed directories inside root snapshots
This commit is contained in:
@@ -266,6 +266,42 @@ def brand_os_release(root):
|
|||||||
write(root, 'etc/os-release', '\n'.join(retained) + '\n')
|
write(root, 'etc/os-release', '\n'.join(retained) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def configure_managed_root_tmpfiles(root):
|
||||||
|
"""Keep automatic systemd directories inside the recoverable root snapshot.
|
||||||
|
|
||||||
|
Same-name overrides retain the installed vendor policy, changing only the
|
||||||
|
three subvolume-creation rules. Do not mask nspawn cleanup/socket rules or
|
||||||
|
the separately mounted /home. Unexpected vendor changes need a review.
|
||||||
|
"""
|
||||||
|
overrides = []
|
||||||
|
for filename, target in (('home.conf', '/srv'),
|
||||||
|
('portables.conf', '/var/lib/portables'),
|
||||||
|
('systemd-nspawn.conf', '/var/lib/machines')):
|
||||||
|
vendor = root_path(root, 'usr/lib/tmpfiles.d/' + filename)
|
||||||
|
destination = 'etc/tmpfiles.d/' + filename
|
||||||
|
override = root_path(root, destination, follow_leaf=False)
|
||||||
|
if vendor.is_symlink() or not vendor.is_file():
|
||||||
|
raise ImageError(f'Required ordinary vendor tmpfiles policy is missing: {filename}')
|
||||||
|
if override.is_symlink() or override.exists():
|
||||||
|
raise ImageError(f'Refusing to replace an existing tmpfiles override: {filename}')
|
||||||
|
lines = vendor.read_text().splitlines(keepends=True)
|
||||||
|
matches = []
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
fields = line.split()
|
||||||
|
if fields and not fields[0].startswith('#') and len(fields) > 1 and fields[1] == target:
|
||||||
|
matches.append((index, fields[0]))
|
||||||
|
if len(matches) != 1 or matches[0][1] not in ('q', 'Q'):
|
||||||
|
raise ImageError(f'Expected one vendor tmpfiles q/Q rule for {target}: {filename}')
|
||||||
|
index = matches[0][0]
|
||||||
|
line = lines[index]
|
||||||
|
start = len(line) - len(line.lstrip(' \t'))
|
||||||
|
lines[index] = line[:start] + 'd' + line[start + 1:]
|
||||||
|
overrides.append((destination, ''.join(lines)))
|
||||||
|
# Validate the complete policy before publishing any same-name overrides.
|
||||||
|
for destination, content in overrides:
|
||||||
|
write(root, destination, content)
|
||||||
|
|
||||||
|
|
||||||
def configure_rootfs(root, source):
|
def configure_rootfs(root, source):
|
||||||
root, source = Path(root), Path(source)
|
root, source = Path(root), Path(source)
|
||||||
# Refuse incomplete integrations before changing the exported copy.
|
# Refuse incomplete integrations before changing the exported copy.
|
||||||
@@ -281,6 +317,7 @@ def configure_rootfs(root, source):
|
|||||||
alias.symlink_to(kernel.name)
|
alias.symlink_to(kernel.name)
|
||||||
sanitize_rootfs(root)
|
sanitize_rootfs(root)
|
||||||
copy_overlay(root, source / 'image/rootfs')
|
copy_overlay(root, source / 'image/rootfs')
|
||||||
|
configure_managed_root_tmpfiles(root)
|
||||||
brand_os_release(root)
|
brand_os_release(root)
|
||||||
write(root, 'etc/hydra/live-mode/dracut.conf', (source / 'image/dracut.conf').read_text())
|
write(root, 'etc/hydra/live-mode/dracut.conf', (source / 'image/dracut.conf').read_text())
|
||||||
root_path(root, 'etc/hydra/live-mode/dracut.conf.d').mkdir()
|
root_path(root, 'etc/hydra/live-mode/dracut.conf.d').mkdir()
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ class ImageTests(unittest.TestCase):
|
|||||||
self.addCleanup(self.tmp.cleanup)
|
self.addCleanup(self.tmp.cleanup)
|
||||||
self.root = Path(self.tmp.name) / 'rootfs'
|
self.root = Path(self.tmp.name) / 'rootfs'
|
||||||
self.root.mkdir()
|
self.root.mkdir()
|
||||||
|
self.vendor_tmpfiles = {
|
||||||
|
'home.conf': '# vendor home policy\nQ /home 0755 - - -\nq /srv 0755 - - -\n',
|
||||||
|
'portables.conf': '# vendor portables policy\nQ /var/lib/portables 0700\n',
|
||||||
|
'systemd-nspawn.conf': (
|
||||||
|
'# vendor nspawn policy\nQ /var/lib/machines 0700 - - -\n'
|
||||||
|
'R! /var/lib/machines/.#*\nR! /.#machine.*\n'
|
||||||
|
'L? /run/systemd/io.systemd.NamespaceResource - - - - /run/host/io.systemd.NamespaceResource\n'
|
||||||
|
'L? /run/systemd/io.systemd.MountFileSystem - - - - /run/host/io.systemd.MountFileSystem\n'),
|
||||||
|
}
|
||||||
|
for name, content in self.vendor_tmpfiles.items():
|
||||||
|
self.file('usr/lib/tmpfiles.d/' + name, content)
|
||||||
|
|
||||||
def file(self, relative, content='preserved'):
|
def file(self, relative, content='preserved'):
|
||||||
path = self.root / relative
|
path = self.root / relative
|
||||||
@@ -35,6 +46,67 @@ class ImageTests(unittest.TestCase):
|
|||||||
self.file('boot/vmlinuz-' + version, 'kernel')
|
self.file('boot/vmlinuz-' + version, 'kernel')
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
def test_managed_root_tmpfiles_creates_ordinary_directories_without_losing_vendor_rules(self):
|
||||||
|
self.kernel()
|
||||||
|
source = Path(__file__).resolve().parents[1]
|
||||||
|
retained = [self.file(path + '/retained-data', 'user data')
|
||||||
|
for path in ('srv', 'var/lib/portables', 'var/lib/machines')]
|
||||||
|
image.configure_rootfs(self.root, source)
|
||||||
|
expected = {
|
||||||
|
'home.conf': '# vendor home policy\nQ /home 0755 - - -\nd /srv 0755 - - -\n',
|
||||||
|
'portables.conf': '# vendor portables policy\nd /var/lib/portables 0700\n',
|
||||||
|
'systemd-nspawn.conf': (
|
||||||
|
'# vendor nspawn policy\nd /var/lib/machines 0700 - - -\n'
|
||||||
|
'R! /var/lib/machines/.#*\nR! /.#machine.*\n'
|
||||||
|
'L? /run/systemd/io.systemd.NamespaceResource - - - - /run/host/io.systemd.NamespaceResource\n'
|
||||||
|
'L? /run/systemd/io.systemd.MountFileSystem - - - - /run/host/io.systemd.MountFileSystem\n'),
|
||||||
|
}
|
||||||
|
for name, content in expected.items():
|
||||||
|
with self.subTest(name=name):
|
||||||
|
override = self.root / 'etc/tmpfiles.d' / name
|
||||||
|
self.assertTrue(override.is_file(), f'missing managed-root policy: {name}')
|
||||||
|
self.assertEqual(override.read_text(), content)
|
||||||
|
self.assertEqual(override.stat().st_mode & 0o777, 0o644)
|
||||||
|
self.assertEqual((self.root / 'usr/lib/tmpfiles.d' / name).read_text(),
|
||||||
|
self.vendor_tmpfiles[name])
|
||||||
|
self.assertEqual([path.read_text() for path in retained], ['user data'] * 3)
|
||||||
|
|
||||||
|
def test_managed_root_tmpfiles_refuses_missing_duplicate_or_changed_vendor_rule(self):
|
||||||
|
import shutil
|
||||||
|
self.kernel()
|
||||||
|
source = Path(__file__).resolve().parents[1]
|
||||||
|
for index, content in enumerate((None, '# no managed path\n', 'd /var/lib/machines 0700\n',
|
||||||
|
'Q /var/lib/machines 0700\nq /var/lib/machines 0700\n',
|
||||||
|
'Q /var/lib/machines 0700\nd /var/lib/machines 0700\n')):
|
||||||
|
with self.subTest(content=content):
|
||||||
|
root = Path(self.tmp.name) / ('drift-' + str(index))
|
||||||
|
shutil.copytree(self.root, root)
|
||||||
|
vendor = root / 'usr/lib/tmpfiles.d/systemd-nspawn.conf'
|
||||||
|
if content is None:
|
||||||
|
vendor.unlink()
|
||||||
|
else:
|
||||||
|
vendor.write_text(content)
|
||||||
|
with self.assertRaisesRegex(image.ImageError, 'tmpfiles'):
|
||||||
|
image.configure_rootfs(root, source)
|
||||||
|
self.assertFalse((root / 'etc/tmpfiles.d/home.conf').exists())
|
||||||
|
|
||||||
|
def test_managed_root_tmpfiles_refuses_existing_override(self):
|
||||||
|
self.kernel()
|
||||||
|
original = self.file('etc/tmpfiles.d/home.conf', 'd /srv 0710 local local -\n')
|
||||||
|
with self.assertRaisesRegex(image.ImageError, 'tmpfiles'):
|
||||||
|
image.configure_rootfs(self.root, Path(__file__).resolve().parents[1])
|
||||||
|
self.assertEqual(original.read_text(), 'd /srv 0710 local local -\n')
|
||||||
|
|
||||||
|
def test_managed_root_tmpfiles_refuses_vendor_symlink(self):
|
||||||
|
self.kernel()
|
||||||
|
original = self.file('usr/lib/tmpfiles.d/other.conf', 'Q /var/lib/machines 0700\n')
|
||||||
|
vendor = self.root / 'usr/lib/tmpfiles.d/systemd-nspawn.conf'
|
||||||
|
vendor.unlink()
|
||||||
|
vendor.symlink_to('other.conf')
|
||||||
|
with self.assertRaisesRegex(image.ImageError, 'tmpfiles'):
|
||||||
|
image.configure_rootfs(self.root, Path(__file__).resolve().parents[1])
|
||||||
|
self.assertEqual(original.read_text(), 'Q /var/lib/machines 0700\n')
|
||||||
|
|
||||||
def test_installs_independent_updater_and_base_recovery_assets(self):
|
def test_installs_independent_updater_and_base_recovery_assets(self):
|
||||||
self.kernel()
|
self.kernel()
|
||||||
source = Path(__file__).resolve().parents[1]
|
source = Path(__file__).resolve().parents[1]
|
||||||
|
|||||||
Reference in New Issue
Block a user