113 lines
6.0 KiB
Python
113 lines
6.0 KiB
Python
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 provisioning import access, image
|
|
from provisioning.image import cloud_enroll, sanitize_files
|
|
from provisioning.bundle 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('provisioning.target.enroll') as enroll:
|
|
with self.assertRaisesRegex(ProvisionError, 'image enrollment interface'):
|
|
cloud_enroll()
|
|
enroll.assert_not_called()
|
|
|
|
def test_sanitization_removes_cloned_identity_and_enrollment_but_preserves_runner(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary).resolve()
|
|
for relative in ('etc/ssh/ssh_host_ed25519_key', 'etc/ssh/ssh_host_ed25519_key.pub',
|
|
'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/provisioning/target.py',
|
|
'tmp/provisioning/image.py'):
|
|
path = root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text('build-specific')
|
|
sanitize_files(root)
|
|
self.assertEqual(list((root / 'etc/ssh').glob('ssh_host_*')), [])
|
|
self.assertEqual((root / 'etc/machine-id').read_text(), '')
|
|
self.assertFalse((root / 'root/.ssh/authorized_keys').exists())
|
|
self.assertFalse((root / 'var/lib/apex-init').exists())
|
|
self.assertFalse((root / 'var/lib/cloud').exists())
|
|
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())
|