fix(apex): preserve environments and restore secure standalone operations
This commit is contained in:
1 parent
6463bad5ee
commit
995ea9bdf9
44 files changed
+1399
-429
No files matched your search
@@ -1,17 +1,90 @@
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine.provisioning.image import cloud_enroll, sanitize_files
|
||||
from engine.provisioning.state import ProvisionError
|
||||
from provisioning import access, image
|
||||
from provisioning.image import cloud_enroll, sanitize_files
|
||||
from provisioning.transport import build_bundle
|
||||
from provisioning.state import atomic_write, safe_path
|
||||
from provisioning.state import ProvisionError
|
||||
|
||||
|
||||
class ImageTests(unittest.TestCase):
|
||||
def test_uploaded_package_installs_runnable_wrappers_and_watchdog(self):
|
||||
commons = Path(__file__).resolve().parents[4]
|
||||
data, digest, _ = build_bundle(commons)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
upload = root / 'tmp'
|
||||
upload.mkdir()
|
||||
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||
archive.extractall(upload, filter='data')
|
||||
|
||||
def mapped(path):
|
||||
return safe_path(root / str(path).lstrip('/'))
|
||||
|
||||
def write(path, content, mode=0o600):
|
||||
return atomic_write(mapped(path), content, mode)
|
||||
|
||||
with patch.object(image, '__file__', str(upload / 'provisioning/image.py')), \
|
||||
patch.object(image, 'safe_path', side_effect=mapped), \
|
||||
patch.object(image, 'atomic_write', side_effect=write):
|
||||
metadata = image.install()
|
||||
installed = root / 'usr/local/lib/apex-provisioner' / digest
|
||||
self.assertEqual(metadata['path'], str(installed))
|
||||
self.assertTrue((installed / 'provisioning/seed.sh').is_file())
|
||||
environment = dict(os.environ)
|
||||
environment.pop('PYTHONPATH', None)
|
||||
for executable in ('apex-provision-image', 'apex-bootstrap'):
|
||||
wrapper = root / 'usr/local/bin' / executable
|
||||
self.assertEqual(wrapper.stat().st_mode & 0o777, 0o755)
|
||||
script = wrapper.read_text().replace('/usr/bin/python3', shlex.quote(sys.executable) + ' -S')
|
||||
result = subprocess.run(['/bin/sh', '-c', script, executable, '--help'],
|
||||
cwd=root, env=environment, capture_output=True,
|
||||
text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('usage:', result.stdout)
|
||||
|
||||
commands = []
|
||||
def runner(args, **kwargs):
|
||||
commands.append(args)
|
||||
return subprocess.CompletedProcess(args, 0, '', '')
|
||||
|
||||
canonical = '/usr/local/lib/apex-provisioner/' + digest
|
||||
with patch.object(access, '__file__', canonical + '/provisioning/access.py'):
|
||||
access.Host(root, runner=runner).arm()
|
||||
service = (root / 'etc/systemd/system/apex-access-rollback.service').read_text()
|
||||
fields = dict(line.split('=', 1) for line in service.splitlines() if '=' in line)
|
||||
self.assertEqual(fields['WorkingDirectory'], canonical)
|
||||
argv = shlex.split(fields['ExecStart'])
|
||||
self.assertEqual(argv[-1], 'tick')
|
||||
result = subprocess.run([sys.executable, '-S', *argv[1:-1], '--help'],
|
||||
cwd=installed, env=environment, capture_output=True,
|
||||
text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn(['systemctl', 'enable', '--now', 'apex-access-rollback.timer'], commands)
|
||||
sanitize_files(root)
|
||||
self.assertFalse((upload / 'provisioning').exists())
|
||||
self.assertTrue((installed / 'provisioning/image.py').is_file())
|
||||
|
||||
def test_watchdog_rejects_uninstalled_package_before_writing_units(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
with self.assertRaisesRegex(access.AccessError, 'installed root-owned bundle'):
|
||||
access.Host(root).arm()
|
||||
self.assertEqual(list(root.iterdir()), [])
|
||||
|
||||
def test_incompatible_image_stops_before_any_enrollment(self):
|
||||
for metadata in ('{}', '{"enrollment_interface":999}'):
|
||||
with self.subTest(metadata=metadata), patch.object(Path, 'read_text', return_value=metadata), \
|
||||
patch('engine.provisioning.target.enroll') as enroll:
|
||||
patch('provisioning.target.enroll') as enroll:
|
||||
with self.assertRaisesRegex(ProvisionError, 'image enrollment interface'):
|
||||
cloud_enroll()
|
||||
enroll.assert_not_called()
|
||||
@@ -23,7 +96,8 @@ class ImageTests(unittest.TestCase):
|
||||
'root/.ssh/authorized_keys', 'home/debian/.ssh/authorized_keys',
|
||||
'etc/machine-id', 'var/lib/dbus/machine-id',
|
||||
'var/lib/apex-init/enrollment.json', 'var/lib/cloud/instance/user-data.txt',
|
||||
'usr/local/lib/apex-provisioner/digest/engine/provisioning/target.py'):
|
||||
'usr/local/lib/apex-provisioner/digest/provisioning/target.py',
|
||||
'tmp/provisioning/image.py'):
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('build-specific')
|
||||
@@ -33,5 +107,6 @@ class ImageTests(unittest.TestCase):
|
||||
self.assertFalse((root / 'root/.ssh/authorized_keys').exists())
|
||||
self.assertFalse((root / 'var/lib/apex-init').exists())
|
||||
self.assertFalse((root / 'var/lib/cloud').exists())
|
||||
self.assertTrue((root / 'usr/local/lib/apex-provisioner/digest/engine/provisioning/target.py').is_file())
|
||||
self.assertFalse((root / 'tmp/provisioning').exists())
|
||||
self.assertTrue((root / 'usr/local/lib/apex-provisioner/digest/provisioning/target.py').is_file())
|
||||
self.assertTrue((root / 'etc/systemd/system/ssh.service.d/10-generate-host-keys.conf').is_file())
|
||||
Reference in new issue
Block a user