├── systems └── .keep ├── docs └── index.rst ├── cleanroom ├── commands │ ├── pkg_desktop.py │ ├── strip_documentation.py │ ├── pkg_intel_kms.py │ ├── set.py │ ├── net_firewall_configure.py │ ├── chmod.py │ ├── ensure_depmod.py │ ├── sed.py │ ├── groupadd.py │ ├── mkdir.py │ ├── append.py │ ├── _pacman_write_package_data.py │ ├── remove.py │ ├── strip_license_files.py │ ├── add_hook.py │ ├── symlink.py │ ├── _teardown.py │ ├── chown.py │ ├── systemd_enable.py │ ├── ensure_no_kernel_install.py │ ├── create.py │ ├── groupmod.py │ ├── _store.py │ ├── crypto_uuid.py │ ├── ensure_no_sysusers.py │ ├── _pacman_keyinit.py │ ├── pkg_quasselcore.py │ ├── run.py │ ├── ensure_no_update_service.py │ ├── _strip_documentation_hook.py │ ├── move.py │ ├── useradd.py │ ├── _depmod_all.py │ ├── net_firewall_open_port.py │ ├── copy.py │ ├── pkg_amd_cpu.py │ ├── swupd.py │ ├── pkg_nvidia_gpu.py │ ├── pkg_fonts.py │ ├── ensure_ldconfig.py │ ├── net_firewall_enable.py │ ├── ensure_no_unused_shell_files.py │ ├── set_timezone.py │ ├── ensure_hwdb.py │ ├── firejail_apps.py │ ├── pkg_kernel.py │ ├── pkg_tmux.py │ ├── strip_development_files.py │ ├── pkg_xorg.py │ ├── tar.py │ ├── pacman.py │ ├── pkg_intel_gpu.py │ ├── pkg_intel_cpu.py │ ├── set_hostname.py │ ├── pkg_avahi.py │ ├── debootstrap.py │ ├── _create_dmverity_fsimage.py │ ├── _restore.py │ ├── usermod.py │ ├── set_machine_id.py │ ├── systemd_set_default.py │ ├── install_certificate.py │ ├── based_on.py │ ├── _create_root_fsimage.py │ ├── _export_directory.py │ ├── create_os_release.py │ ├── sshd_set_hostkeys.py │ ├── sign_efi_binary.py │ ├── _test.py │ └── pkg_glusterfs.py ├── buildcontainer │ └── scripts │ │ ├── wrapper.sh │ │ ├── wrapper_post.sh │ │ └── wrapper_fail.sh ├── __init__.py ├── helper │ ├── __init__.py │ ├── debian │ │ └── __init__.py │ ├── archlinux │ │ └── __init__.py │ ├── systemd.py │ ├── group.py │ └── btrfs.py ├── firestarter │ ├── __init__.py │ ├── deploytarget.py │ ├── installtarget.py │ ├── copyinstalltarget.py │ ├── qemuinstalltarget.py │ ├── mountinstalltarget.py │ ├── tarballinstalltarget.py │ └── containerfsinstalltarget.py ├── execobject.py ├── preflight.py ├── exceptions.py ├── executor.py └── location.py ├── examples ├── type-bootable │ └── extra-files │ │ └── usr │ │ └── demo_file ├── type-vm.def ├── tests │ ├── 000-filesystem │ ├── 006-var-lib-pacman │ ├── 005-etc-systemd │ ├── 050-locale │ ├── 002-systemd-users │ ├── 001-root-password │ ├── README.md │ ├── 100-net-firewall-iptables │ ├── 065-systemd-usr-perms │ └── 060-systemd-usr-links ├── system-basic.def ├── system-pythondev.def ├── system-repart.def ├── type-networkedbase.def ├── type-cppdevcontainer.def ├── type-devcontainer.def ├── type-basecontainer.def ├── type-baremetal.def ├── type-server.def ├── type-desktop.def ├── system-example.def ├── system-dracut.def ├── system-example-desktop.def └── README.md ├── setup.cfg ├── .gitignore ├── clrm ├── mypy.sh ├── firestarter ├── Pipfile ├── .github └── FUNDING.yml ├── TODO.md ├── tests ├── test_commands.py ├── test_cmd_set.py ├── test_exceptions.py ├── test_cmd_run.py ├── test_location.py ├── test_helper_disk.py └── test_helper_group.py ├── setup.py └── README.md /systems/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | FIXME: Write docs... 2 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_desktop.py: -------------------------------------------------------------------------------- 1 | pkg_gnome.py -------------------------------------------------------------------------------- /examples/type-bootable/extra-files/usr/demo_file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cleanroom/buildcontainer/scripts/wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "$@" 4 | 5 | "$@" 6 | 7 | exit $? 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = --verbose 6 | python_files = tests/*.py 7 | -------------------------------------------------------------------------------- /cleanroom/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """cleanroom Module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /cleanroom/helper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """cleanroom Module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /cleanroom/firestarter/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """cleanroom Module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /cleanroom/helper/debian/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """cleanroom.helper.debian Module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /cleanroom/helper/archlinux/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """cleanroom.helper.archlinux Module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /examples/type-vm.def: -------------------------------------------------------------------------------- 1 | # Very basic setup for VMs 2 | 3 | based_on type-bootable 4 | 5 | remove /usr/lib/firmware/iwlwifi* /usr/lib/firmware/ath10k/* 6 | /usr/lib/firmware/intel/* 7 | force=True recursive=True 8 | -------------------------------------------------------------------------------- /cleanroom/buildcontainer/scripts/wrapper_post.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | "$@" 4 | 5 | EXIT_CODE=$? 6 | 7 | echo "Post shell after clrm finished with exit code ${EXIT_CODE}" 8 | 9 | /bin/bash 10 | 11 | exit ${EXIT_CODE} 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .mypy_cache 3 | *.pyc 4 | Pipfile.lock 5 | 6 | *.user* 7 | .vscode 8 | .idea 9 | *.swp 10 | .spyproject 11 | 12 | .eggs/* 13 | systems/* 14 | .pytest_cache/* 15 | *.egg-info 16 | build/* 17 | dist/* 18 | -------------------------------------------------------------------------------- /examples/tests/000-filesystem: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | test -c "dev/null" || fail "/dev/null is missing or no char device." 9 | 10 | echo "Devices look sane, ok" 11 | 12 | -------------------------------------------------------------------------------- /examples/tests/006-var-lib-pacman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | test -d var/lib/pacman && fail "/var/lib/pacman exists, fail" 1 9 | 10 | echo "No /var/lib/pacman, ok" 11 | 12 | -------------------------------------------------------------------------------- /clrm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Main CleanRoom binary. 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | import cleanroom.main as main 10 | 11 | 12 | if __name__ == '__main__': 13 | main.run() 14 | -------------------------------------------------------------------------------- /examples/system-basic.def: -------------------------------------------------------------------------------- 1 | # A very basic system that should boot 2 | 3 | based_on type-baremetal 4 | 5 | set_hostname basic pretty=Basic 6 | set_machine_id bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 7 | 8 | export borg_repository efi_emulator=/mnt/btrfs/clrm/examples/CloverV2 9 | -------------------------------------------------------------------------------- /examples/tests/005-etc-systemd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | test -z "$(ls -A etc/systemd/system)" \ 9 | || fail "/etc/systemd/system is not empty, fail" 1 10 | 11 | echo "/etc/systemd is empty, ok" 12 | 13 | -------------------------------------------------------------------------------- /cleanroom/buildcontainer/scripts/wrapper_fail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "$@" 4 | "$@" 5 | 6 | EXIT_CODE=$? 7 | 8 | if test $EXIT_CODE -ne 0 ; then 9 | echo "Fail shell after clrm finished with exit code ${EXIT_CODE}." 10 | /bin/bash 11 | fi 12 | 13 | exit $EXIT_CODE 14 | -------------------------------------------------------------------------------- /mypy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | FILE="${1}" 4 | 5 | if test $(basename "${FILE}") == '__init__.py'; then 6 | exit 0 7 | fi 8 | 9 | echo "::::::::: ${FILE}:" 10 | grep "import typing" "${FILE}" > /dev/null || echo "!!!!!!! ${FILE}: typing is not imported" 11 | mypy "${FILE}" 12 | -------------------------------------------------------------------------------- /examples/tests/050-locale: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | grep "^LC_MESSAGES=en_US" etc/locale.conf > /dev/null \ 9 | || fail "LC_MESSAGES not set up in /etc/locale.conf" 2 10 | 11 | echo "Locale has been set up, ok" 12 | 13 | -------------------------------------------------------------------------------- /examples/tests/002-systemd-users: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | grep '^systemd-resolve:x:' etc/passwd || fail "No system-resolve user set in /etc/passwd, fail", 1 9 | 10 | echo "systemd-resolve user found in /etc/passwd, ok" 11 | 12 | -------------------------------------------------------------------------------- /firestarter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Export cleanroom builds into various different formats. 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.main import run 10 | 11 | 12 | if __name__ == '__main__': 13 | run() 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pyparsing = "*" 8 | pipenv = "*" 9 | 10 | [dev-packages] 11 | pytest = "*" 12 | rope = "*" 13 | black = "*" 14 | 15 | [requires] 16 | python_version = "3.8" 17 | 18 | [pipenv] 19 | allow_prereleases = true 20 | -------------------------------------------------------------------------------- /examples/tests/001-root-password: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | grep '^root:[x!]?:' etc/shadow && fail "No root password set in /etc/shadow, fail", 1 9 | grep ':\\\$' etc/shadow && fail "Broken password set in /etc/shadow, fail", 2 10 | 11 | echo "Root password set in /etc/shadow, ok" 12 | 13 | -------------------------------------------------------------------------------- /examples/system-pythondev.def: -------------------------------------------------------------------------------- 1 | # Configuration for python development container 2 | 3 | ### Basic python development environment 4 | 5 | based_on type-cppdevcontainer 6 | 7 | set_hostname pythondev pretty=Python-Dev 8 | set_machine_id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 9 | 10 | pacman python-pip 11 | 12 | # Export a image that can be used as a container: 13 | export borg_repository usr_only=False 14 | -------------------------------------------------------------------------------- /examples/tests/README.md: -------------------------------------------------------------------------------- 1 | # This directory contains test executables that will be run 2 | # after each system is set up. 3 | # 4 | # The tests will be run outside of the system. 5 | # 6 | # The work directory will be the top level directory of the 7 | # system's filesystem. 8 | # 9 | # The following environment variables will be set: 10 | # * TIMESTAMP 11 | # * SYSTEM_NAME 12 | # * BASE_SYSTEM_NAME 13 | # * ROOT 14 | 15 | -------------------------------------------------------------------------------- /cleanroom/execobject.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Parse system definition files. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.location import Location 9 | 10 | 11 | import typing 12 | 13 | 14 | class ExecObject(typing.NamedTuple): 15 | location: Location 16 | command: str 17 | args: typing.Tuple[typing.Any, ...] 18 | kwargs: typing.Dict[str, typing.Any] 19 | -------------------------------------------------------------------------------- /examples/system-repart.def: -------------------------------------------------------------------------------- 1 | # A very basic system that should boot 2 | 3 | based_on type-baremetal 4 | 5 | set_hostname repart pretty=Repart 6 | set_machine_id cccccccccccccccccccccccccccccccc 7 | 8 | add_partition 10-efi type=esp minSize=128M maxSize=1G weight=250 9 | add_partition 20-swap type=swap minSize=1G maxSize=4G weight=250 priority=1000 10 | add_partition 30-rest type=linux-generic label=fs_btrfs 11 | 12 | export borg_repository 13 | -------------------------------------------------------------------------------- /examples/tests/100-net-firewall-iptables: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | if test -e usr/lib/iptables ; then 9 | test -e etc/iptables/iptables.rules || fail "No ipv4 iptables configuration." 1 10 | test -e etc/iptables/ip6tables.rules || fail "No ipv6 iptables configuration." 2 11 | 12 | echo "Iptables installed and configured, ok" 13 | else 14 | echo "No iptables, ok." 15 | fi 16 | -------------------------------------------------------------------------------- /examples/type-networkedbase.def: -------------------------------------------------------------------------------- 1 | # *Very* basic networked base installation 2 | 3 | based_on type-base 4 | 5 | pacman inetutils 6 | 7 | systemd_enable systemd-networkd.service systemd-resolved.service 8 | remove /etc/resolv.conf 9 | symlink /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf 10 | 11 | mkdir /etc/systemd/resolved.conf.d mode=0o755 12 | create /etc/systemd/resolved.conf.d/no_fallback_dns <<<< 13 | [Resolve] 14 | FallbackDNS= 15 | >>>> mode=0o644 16 | 17 | # Enable firewalling: 18 | net_firewall_enable 19 | -------------------------------------------------------------------------------- /examples/type-cppdevcontainer.def: -------------------------------------------------------------------------------- 1 | # Generic base for development containers 2 | 3 | # This is the base for all development containers 4 | 5 | based_on type-devcontainer 6 | 7 | pacman 8 | autoconf automake 9 | 10 | bison 11 | 12 | clang clang-tools-extra clazy llvm gperf 13 | 14 | flex 15 | 16 | gcc gettext 17 | 18 | libtool 19 | 20 | make 21 | 22 | patch pkg-config 23 | 24 | strace 25 | 26 | texinfo 27 | 28 | ccache 29 | 30 | ninja meson cmake 31 | 32 | create /etc/ccache.conf <<<>>> mode=0o644 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [hunger] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /examples/tests/065-systemd-usr-perms: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | TOP=$(pwd) 9 | SD="${TOP}/usr/lib/systemd/system" 10 | 11 | for D in $(cd "${SD}" ; find . -type d) ; do 12 | ( cd "${SD}" ; stat "${D}" ) | grep '^Access: (0755/' > /dev/null \ 13 | || fail "Systemd directory ${D} with unexpected permissions." 2 14 | done 15 | 16 | echo "Systemd directory permissions ok." 17 | 18 | for F in $(cd "${SD}" ; find . -type f) ; do 19 | ( cd "${SD}" ; stat "${F}" ) | grep '^Access: (0644/' > /dev/null \ 20 | || fail "Systemd unit ${F} with unexpected permissions." 3 21 | done 22 | 23 | echo "Systemd unit permissions ok." 24 | 25 | -------------------------------------------------------------------------------- /cleanroom/helper/systemd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Helpers for systemd inteaction. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from ..systemcontext import SystemContext 9 | from .run import run 10 | 11 | import typing 12 | 13 | 14 | def systemd_enable( 15 | system_context: SystemContext, 16 | *services: str, 17 | systemctl_command: str, 18 | **kwargs: typing.Any, 19 | ) -> None: 20 | """Enable systemd service.""" 21 | all_args = [ 22 | f"--root={system_context.fs_directory}", 23 | ] 24 | if kwargs.get("user", False): 25 | all_args.append("--global") 26 | all_args.append("enable") 27 | run(systemctl_command, *all_args, *services) 28 | -------------------------------------------------------------------------------- /cleanroom/firestarter/deploytarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Generic deployment target 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | 11 | import typing 12 | 13 | 14 | class DeployInstallTarget(InstallTarget): 15 | def __init__(self) -> None: 16 | super().__init__( 17 | "deploy", "Deploy the machine as specified in its deployment information" 18 | ) 19 | 20 | def setup_subparser(self, subparser: typing.Any) -> None: 21 | pass 22 | 23 | def __call__( 24 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 25 | ) -> int: 26 | return 0 27 | -------------------------------------------------------------------------------- /examples/type-devcontainer.def: -------------------------------------------------------------------------------- 1 | # Generic base for development containers 2 | 3 | # This is the base for all development containers 4 | 5 | based_on type-basecontainer 6 | 7 | pacman 8 | pacman # Install pacman! 9 | 10 | binutils 11 | 12 | diffutils 13 | 14 | gawk 15 | 16 | strace 17 | 18 | texinfo 19 | 20 | fish 21 | 22 | gdb 23 | 24 | ltrace strace valgrind iputils iproute2 wget 25 | 26 | pngcrush tk 27 | 28 | patch perf 29 | 30 | strace 31 | 32 | tokei 33 | 34 | gedit gedit-plugins git 35 | 36 | xdg-user-dirs 37 | 38 | mesa-libgl opencl-mesa 39 | 40 | xterm 41 | 42 | jq 43 | 44 | pkg_vim 45 | pkg_manpages 46 | 47 | create /etc/sudoers.d/90-allow-devel <<<<%devel ALL=(ALL) ALL>>>> mode=0o640 48 | -------------------------------------------------------------------------------- /examples/type-basecontainer.def: -------------------------------------------------------------------------------- 1 | # *Very* basic arch setup for containers 2 | 3 | # This is the base for all other containers 4 | 5 | based_on type-networkedbase 6 | 7 | sed 's/^CHASSIS=.*$$/CHASSIS=container/' /etc/machine.info 8 | 9 | append /etc/securetty <<<< 10 | # Make containers work: 11 | pts/0 12 | pts/1 13 | pts/2 14 | pts/3 15 | pts/4 16 | pts/5 17 | pts/6 18 | >>>> 19 | 20 | remove /usr/lib/systemd/system/fstrim.timer 21 | /usr/lib/systemd/system/fstrim.service 22 | 23 | # Do not remove sockets (which might be bind-mounted into the container): 24 | remove /usr/lib/tmpfiles.d/x11.conf 25 | 26 | # Make sure systemd-boot stuff is gone and stays gone: 27 | add_hook _teardown remove /usr/lib/systemd/boot 28 | /usr/share/systemd/bootctl /usr/bin/bootctl 29 | recursive=True force=True 30 | -------------------------------------------------------------------------------- /cleanroom/firestarter/installtarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Firestarter InstallTarget class. 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | import typing 10 | 11 | 12 | class InstallTarget(object): 13 | def __init__(self, name: str, help_string: str) -> None: 14 | self._name = name 15 | self._help_string = help_string 16 | 17 | def __call__( 18 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 19 | ) -> int: 20 | assert False 21 | 22 | def setup_subparser(self, subparser: typing.Any) -> None: 23 | assert False 24 | 25 | @property 26 | def help_string(self) -> str: 27 | return self._help_string 28 | 29 | @property 30 | def name(self) -> str: 31 | return self._name 32 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Pacman: 2 | * Remove /usr/lib/libalpm with hooks, etc.? 3 | * Use --hookdir to override default hooks? 4 | 5 | option infrastructure: 6 | * option_cpu_intel 7 | * option_gpu_intel 8 | 9 | * Make verity partition name configurable (uuid is used anyway:-) 10 | * Fix imager not to depend on clrm_* for root partitions [fixme] 11 | 12 | * No /var/log/journal in VM. Normal? 13 | * /var/log/pacman.log: Where does that come from? pacstrap 14 | should delete that in _teardown hook 15 | 16 | * Remove jack [test] 17 | 18 | * export_rootfs: 19 | * create_export_directory -- should work 20 | * tarball creation and storage 21 | 22 | * alacritty support on ron is missing:-/ 23 | 24 | * Use erofs over squashfs (once available) 25 | * Move to dracut for initrd generation 26 | 27 | * Remove C! lines from usr/lib/tmpfiles.d/etc.conf 28 | -------------------------------------------------------------------------------- /examples/type-baremetal.def: -------------------------------------------------------------------------------- 1 | # Very basic setup for bare metal images 2 | 3 | based_on type-bootable 4 | 5 | # BAREMETAL_SETUP 6 | pacman bridge-utils borg fish gptfdisk hdparm htop 7 | 8 | fzf 9 | 10 | sbsigntools tinyserial wget 11 | 12 | tlp tlp-rdw acpi_call ethtool smartmontools x86_energy_perf_policy 13 | 14 | e2fsprogs xfsprogs 15 | 16 | ripgrep rsync 17 | 18 | vim strace 19 | 20 | pkg_tmux 21 | 22 | create /etc/profile.d/timeout.sh <<<>>> mode=0o644 26 | 27 | # Make sure we have a vconsole.conf file, mkinitcpio will need that later 28 | create /etc/vconsole.conf <<<<>>>> mode=0o644 29 | 30 | systemd_enable fstrim.timer smartd.service tlp.service 31 | # This conflicts with tlp: 32 | remove /usr/lib/systemd/system/systemd-rfkill.service 33 | /usr/lib/systemd/system/systemd-rfkill.socket 34 | 35 | -------------------------------------------------------------------------------- /cleanroom/firestarter/copyinstalltarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Copy Install Target 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | 11 | from shutil import copy 12 | import typing 13 | 14 | 15 | class CopyInstallTarget(InstallTarget): 16 | def __init__(self) -> None: 17 | super().__init__("copy", "copy the image to a directory, device or file") 18 | 19 | def setup_subparser(self, subparser: typing.Any) -> None: 20 | subparser.add_argument( 21 | dest="target", action="store", help="The target to copy into.", 22 | ) 23 | 24 | def __call__( 25 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 26 | ) -> int: 27 | assert parse_result.target 28 | 29 | copy(image_file, parse_result.target) 30 | 31 | return 0 32 | -------------------------------------------------------------------------------- /cleanroom/preflight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The Context the generation will run in. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from .exceptions import PreflightError 9 | from .printer import debug, fail, success 10 | 11 | import os 12 | import typing 13 | 14 | 15 | def preflight_check( 16 | title: str, func: typing.Callable[[], None], *, ignore_errors: bool = False 17 | ) -> None: 18 | try: 19 | func() 20 | except PreflightError: 21 | fail(f'Preflight Check "{title}" failed', ignore=ignore_errors) 22 | if not ignore_errors: 23 | raise 24 | else: 25 | success(f'Preflight Check "{title}" passed', verbosity=2) 26 | 27 | 28 | def users_check() -> None: 29 | """Check tha the script is running as root.""" 30 | if os.geteuid() == 0: 31 | debug("Running as root.") 32 | else: 33 | raise PreflightError('Not running as user "root".') 34 | -------------------------------------------------------------------------------- /examples/tests/060-systemd-usr-links: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | function fail { 4 | echo "$1" 5 | exit $2 6 | } 7 | 8 | TOP=$(pwd) 9 | SD="${TOP}/usr/lib/systemd/system" 10 | 11 | for L in $(cd "${SD}" ; find . -type l) ; do 12 | LT=$(cd "${SD}" && readlink "${L}" | sed -e 's!^\./!!') 13 | if test "x$LT" = "x/dev/null" ; then 14 | echo "Systemd: Symlink ${L} is a mask, ok." 15 | elif [[ "$(dirname "${L}")" == "." ]]; then 16 | # top level: 17 | if [[ "${LT}" != */* ]]; then 18 | echo "Systemd: Symlink ${L} is an alias for ${LT}, ok" 19 | else 20 | fail "Systemd: Symlink ${L} points to a strange place ${LT}, fail" 1 21 | fi 22 | else 23 | # (target.wants) sub folder: 24 | if [[ "${LT}" == ../* ]]; then 25 | echo "Systemd: Symlink ${L} is relative, ok" 26 | else 27 | fail "Systemd: Symlink ${L} is not relative ($LT), fail" 1 28 | fi 29 | fi 30 | done 31 | 32 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/python 2 | # """Test for the built-in print_commands of cleanroom. 3 | # 4 | # @author: Tobias Hunger 5 | # """ 6 | 7 | 8 | import pytest # type: ignore 9 | 10 | import os 11 | import sys 12 | 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 14 | 15 | import cleanroom.exceptions as ex 16 | 17 | 18 | def test_based_on_command(parser): 19 | """Test based with a name.""" 20 | parser.parse_and_verify_string( 21 | " based_on foo\n", "foo", [("based_on", ("foo",), {}, 1)] 22 | ) 23 | 24 | 25 | # Error cases: 26 | @pytest.mark.parametrize( 27 | "test_input", 28 | [ 29 | pytest.param(" based_on\n", id="no system"), 30 | pytest.param(" based_on f!00\n", id="invalid system name"), 31 | ], 32 | ) 33 | def test_based_on_errors(parser, test_input): 34 | """Test an image without name.""" 35 | with pytest.raises(ex.ParseError): 36 | parser.parse_and_verify_string(test_input, "", []) 37 | -------------------------------------------------------------------------------- /examples/type-server.def: -------------------------------------------------------------------------------- 1 | # Very basic arch setup for server use 2 | 3 | based_on type-baremetal 4 | 5 | sed '/^CHASSIS=/ cCHASSIS="server"' /etc/machine.info 6 | 7 | pacman qemu-headless 8 | 9 | # Remove temporary IPv6 addresses for servers: 10 | remove /etc/sysctl.d/ipv6_tempaddr.conf 11 | 12 | # Have a bridge: 13 | create /usr/lib/systemd/network/10-extbr0.netdev <<<<[Match] 14 | Virtualization=no 15 | 16 | [NetDev] 17 | Name=extbr0 18 | Kind=bridge 19 | >>>> mode=0o644 20 | 21 | create /usr/lib/systemd/network/20-extbr0.network <<<<[Match] 22 | Virtualization=no 23 | Name=extbr0 24 | 25 | [Network] 26 | IPForward=yes 27 | >>>> mode=0o644 28 | 29 | create /usr/lib/systemd/network/80-enstar.link <<<<[Match] 30 | Virtualization=no 31 | Name=en* 32 | 33 | [Link] 34 | WakeOnLand=off 35 | NamePolicy=kernel database onboard slot path 36 | MACAddressPolicy=persistent 37 | >>>> mode=0o644 38 | 39 | create /usr/lib/systemd/network/85-enstar.network <<<<[Match] 40 | Virtualization=no 41 | Name=en* 42 | 43 | [Network] 44 | Bridge=extbr0 45 | >>>> mode=0o644 46 | -------------------------------------------------------------------------------- /examples/type-desktop.def: -------------------------------------------------------------------------------- 1 | # Very basic arch setup for desktop use 2 | 3 | # This is the base for all desktop systems 4 | 5 | based_on type-baremetal 6 | 7 | sed '/^CHASSIS=/ cCHASSIS="desktop"' /etc/machine.info 8 | 9 | sed '/^\\s*#*\\s*ProcessSizeMax=/ cProcessSizeMax=10M' /etc/systemd/journald.conf 10 | sed '/^\\s*#*\\s*SystemUseMax=/ cSystemUseMax=100M' /etc/systemd/coredump.conf 11 | 12 | systemd_set_default graphical.target 13 | 14 | append /etc/fstab <<<< 15 | PARTLABEL=swap swap swap defaults 0 0 16 | 17 | LABEL=fs_btrfs /home btrfs compress=zstd,subvol=@home,nofail 0 0 18 | >>>> 19 | 20 | pkg_desktop 21 | 22 | pacman 23 | alacritty 24 | chromium 25 | dmidecode 26 | firejail 27 | flatpak fuse3 28 | git 29 | keybase kbfs keybase-gui 30 | ntfs-3g 31 | ovmf 32 | perf 33 | qemu 34 | smbclient sshfs sudo 35 | tcpdump 36 | wl-clipboard 37 | 38 | ### Fixup smbclient: 39 | mkdir /etc/samba 40 | create /etc/samba/smb.conf <<<<>>>> mode=0o644 41 | 42 | # Allow fuse users: 43 | sed '/user_allow_other$/ cuser_allow_other' /etc/fuse.conf 44 | 45 | firejail_apps chromium 46 | -------------------------------------------------------------------------------- /cleanroom/commands/strip_documentation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """strip_documentation command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class StripDocumentationCommand(Command): 16 | """The strip_documentation Command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "strip_documentation", 22 | help_string="Strip away documentation files.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | self._add_hook(location, system_context, "export", "_strip_documentation_hook") 42 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_intel_kms.py: -------------------------------------------------------------------------------- 1 | """pkg_intel_kms command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.location import Location 9 | from cleanroom.systemcontext import SystemContext 10 | 11 | import typing 12 | 13 | 14 | class PkgIntelKmsCommand(Command): 15 | """The pkg_intel_kms command.""" 16 | 17 | def __init__(self, **services: typing.Any) -> None: 18 | """Constructor.""" 19 | super().__init__( 20 | "pkg_intel_kms", 21 | help_string="Set up Kernel Mode Setting for Intel GPU.", 22 | file=__file__, 23 | **services 24 | ) 25 | 26 | def validate( 27 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 28 | ) -> None: 29 | """Validate the arguments.""" 30 | self._validate_no_arguments(location, *args, **kwargs) 31 | 32 | def __call__( 33 | self, 34 | location: Location, 35 | system_context: SystemContext, 36 | *args: typing.Any, 37 | **kwargs: typing.Any 38 | ) -> None: 39 | """Execute command.""" 40 | 41 | # enable kms: 42 | system_context.set_or_append_substitution( 43 | "INITRD_EXTRA_MODULES", "intel_agp i915" 44 | ) 45 | -------------------------------------------------------------------------------- /cleanroom/commands/set.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """set command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class SetCommand(Command): 16 | """The set command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "set", 22 | syntax=" ", 23 | help_string="Set up a substitution.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_arguments_exact( 33 | location, 2, '"{}" needs a key and a value.', *args, **kwargs 34 | ) 35 | 36 | def __call__( 37 | self, 38 | location: Location, 39 | system_context: SystemContext, 40 | *args: typing.Any, 41 | **kwargs: typing.Any 42 | ) -> None: 43 | """Execute command.""" 44 | print(args) 45 | system_context.set_substitution(args[0], args[1]) 46 | -------------------------------------------------------------------------------- /cleanroom/commands/net_firewall_configure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """net_firewall_configure command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.archlinux.iptables import install_rules 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class NetFirewallConfigureCommand(Command): 17 | """The net_firewall_configure command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "net_firewall_configure", 23 | help_string="Set up basic firewall.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any 40 | ) -> None: 41 | """Execute command.""" 42 | install_rules(location, system_context) 43 | -------------------------------------------------------------------------------- /cleanroom/commands/chmod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """chmod command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import chmod 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class ChmodCommand(Command): 17 | """The chmod command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "chmod", 23 | syntax=" +", 24 | help_string="Chmod a file or files.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_arguments_at_least( 34 | location, 2, '"{}" takes a mode and one ' "or more files.", *args, **kwargs 35 | ) 36 | 37 | def __call__( 38 | self, 39 | location: Location, 40 | system_context: SystemContext, 41 | *args: typing.Any, 42 | **kwargs: typing.Any 43 | ) -> None: 44 | """Execute command.""" 45 | chmod(system_context, *args, **kwargs) 46 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_depmod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_depmod command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class EnsureDepmodCommand(Command): 16 | """The ensure_depmod command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "ensure_depmod", 22 | help_string="Ensure that depmod is run for all kernels.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | location.set_description("Run ldconfig") 42 | self._add_hook( 43 | location, system_context, "export", "_depmod_all", 44 | ) 45 | -------------------------------------------------------------------------------- /cleanroom/commands/sed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """sed command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.run import run 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class SedCommand(Command): 17 | """The sed command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "sed", 23 | syntax=" ", 24 | help_string="Run sed on a file.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_arguments_exact( 34 | location, 2, '"{}" needs a pattern and a file.', *args, **kwargs 35 | ) 36 | 37 | def __call__( 38 | self, 39 | location: Location, 40 | system_context: SystemContext, 41 | *args: typing.Any, 42 | **kwargs: typing.Any 43 | ) -> None: 44 | """Execute command.""" 45 | run("/usr/bin/sed", "-i", "-e", args[0], system_context.file_name(args[1])) 46 | -------------------------------------------------------------------------------- /cleanroom/firestarter/qemuinstalltarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Firestarter: Simple qemu runner 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | import cleanroom.firestarter.qemutools as qemu_tool 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class QemuInstallTarget(InstallTarget): 17 | def __init__(self) -> None: 18 | super().__init__("qemu", "Boot image in qemu") 19 | 20 | def __call__( 21 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 22 | ) -> int: 23 | if not "DISPLAY" in os.environ: 24 | print("No DISPLAY variable set: Can not start qemu.") 25 | exit(1) 26 | 27 | clrm_device = f"{image_file}:raw:read-only" 28 | if parse_result.usb_clrm: 29 | clrm_device += ":usb" 30 | 31 | return qemu_tool.run_qemu( 32 | parse_result, drives=[clrm_device], work_directory=tmp_dir, 33 | ) 34 | 35 | def setup_subparser(self, subparser: typing.Any) -> None: 36 | qemu_tool.setup_parser_for_qemu(subparser) 37 | subparser.add_argument( 38 | "--usb-clrm", 39 | dest="usb_clrm", 40 | action="store_true", 41 | help="Put CLRM onto a virtual USB stick", 42 | ) 43 | -------------------------------------------------------------------------------- /cleanroom/commands/groupadd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """groupadd command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class GroupaddCommand(Command): 16 | """The groupadd command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "groupadd", 22 | syntax=" [force=False] " "[system=False] [gid=]", 23 | help_string="Add a group.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_args_exact(location, 1, '"{}" needs exactly one name.', *args) 33 | self._validate_kwargs(location, ("force", "gid", "system"), **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any 41 | ) -> None: 42 | """Execute command.""" 43 | self._service("group_helper").groupadd( 44 | args[0], **kwargs, root_directory=system_context.fs_directory 45 | ) 46 | -------------------------------------------------------------------------------- /cleanroom/commands/mkdir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """mkdir command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import makedirs 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class MkdirCommand(Command): 17 | """The mkdir command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "mkdir", 23 | syntax="+ [user=uid] [group=gid] " "[mode=0o755] [exist_ok=False]", 24 | help_string="Create a new directory.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_args_at_least( 34 | location, 1, '"{}" needs at least one directory ' "to create.", *args 35 | ) 36 | self._validate_kwargs(location, ("user", "group", "mode", "exist_ok"), **kwargs) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any 44 | ) -> None: 45 | """Execute command.""" 46 | makedirs(system_context, *args, **kwargs) 47 | -------------------------------------------------------------------------------- /cleanroom/commands/append.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """append command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import append_file 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class AppendCommand(Command): 17 | """The append command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "append", 23 | syntax=" ", 24 | help_string="Append contents to file.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_arguments_exact( 34 | location, 2, '"{}" needs a file and contents ' "to append to it.", *args 35 | ) 36 | self._validate_kwargs(location, ("force",), **kwargs) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any 44 | ) -> None: 45 | """Execute command.""" 46 | to_write = args[1].encode("utf-8") 47 | append_file(system_context, args[0], to_write, **kwargs) 48 | -------------------------------------------------------------------------------- /tests/test_cmd_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Test for the set command of cleanroom. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import pytest # type: ignore 9 | 10 | import os 11 | import sys 12 | 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 14 | 15 | from cleanroom.commandmanager import call_command 16 | from cleanroom.commands.set import SetCommand 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ("key", "value", "expected"), 21 | [ 22 | pytest.param("FOO", "bar", "bar", id="basic"), 23 | pytest.param("KNOWN", "bar", "bar", id="override KNOWN"), 24 | pytest.param("KNOWN", "${KNOWN} bar", "some value bar", id="Append KNOWN"), 25 | pytest.param("KNOWN", "bar ${KNOWN}", "bar some value", id="Prepend KNOWN"), 26 | pytest.param( 27 | "KNOWN", 28 | "bar ${KNOWN} foo", 29 | "bar some value foo", 30 | id="Prepend & Append KNOWN", 31 | ), 32 | ], 33 | ) 34 | def test_cmd_set(system_context, location, key, value, expected): 35 | """Test map_base.""" 36 | system_context.set_substitution("TEST", "") 37 | system_context.set_substitution("ROOT_DIR", "/some/place") 38 | 39 | system_context.set_substitution("KNOWN", "some value") 40 | 41 | set_cmd = SetCommand() 42 | 43 | call_command(location, system_context, set_cmd, key, value) 44 | 45 | result = system_context.substitution(key, "") 46 | 47 | assert result == expected 48 | -------------------------------------------------------------------------------- /cleanroom/commands/_pacman_write_package_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_pacman_write_package_data command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.archlinux.pacman import pacman_report 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class PacmanWritePackageDataCommand(Command): 18 | """The pacman command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "_pacman_write_package_data", 24 | help_string="Write pacman package data into the filesystem.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_no_arguments(location, *args, **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any 41 | ) -> None: 42 | """Execute command.""" 43 | pacman_report( 44 | system_context, 45 | system_context.file_name("/usr/lib/pacman"), 46 | pacman_command=self._binary(Binaries.PACMAN), 47 | ) 48 | -------------------------------------------------------------------------------- /cleanroom/commands/remove.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """remove command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.helper.file import remove 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class RemoveCommand(Command): 17 | """The copy command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "remove", 23 | syntax=" [force=True] [recursive=True] [outside=False]", 24 | help_string="remove files within the system.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_args_at_least( 34 | location, 35 | 1, 36 | '"{}" needs at least one file or ' "directory to remove.", 37 | *args 38 | ) 39 | self._validate_kwargs(location, ("force", "recursive", "outside"), **kwargs) 40 | 41 | def __call__( 42 | self, 43 | location: Location, 44 | system_context: SystemContext, 45 | *args: typing.Any, 46 | **kwargs: typing.Any 47 | ) -> None: 48 | """Execute command.""" 49 | remove(system_context, *args, **kwargs) 50 | -------------------------------------------------------------------------------- /cleanroom/commands/strip_license_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """strip_license_files command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class StripLicenseFilesCommand(Command): 16 | """The strip_license_files Command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "strip_license_files", 22 | help_string="Strip away license files.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | location.set_description("Strip license files") 42 | self._add_hook( 43 | location, 44 | system_context, 45 | "export", 46 | "remove", 47 | "/usr/share/licenses/*", 48 | "/usr/share/package-licenses/*", 49 | recursive=True, 50 | force=True, 51 | ) 52 | -------------------------------------------------------------------------------- /cleanroom/commands/add_hook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """add_hook command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class AddHookCommand(Command): 16 | """The add_hook command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "add_hook", 22 | syntax=" " "* [message=] []", 23 | help_string="Add a hook running command with " "arguments.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_args_at_least( 33 | location, 34 | 1, 35 | '"{}" needs a hook name and a ' "command and optional arguments.", 36 | *args 37 | ) 38 | 39 | def __call__( 40 | self, 41 | location: Location, 42 | system_context: SystemContext, 43 | *args: typing.Any, 44 | message: str = "", 45 | **kwargs: typing.Any 46 | ) -> None: 47 | """Execute command.""" 48 | location.set_description(message) 49 | self._add_hook(location, system_context, args[0], args[1], *args[2:], **kwargs) 50 | -------------------------------------------------------------------------------- /cleanroom/commands/symlink.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """symlink command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import symlink 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class SymlinkCommand(Command): 17 | """The symlink command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "symlink", 23 | syntax=" [work_directory=BASE]", 24 | help_string="Create a symlink.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_args_exact( 34 | location, 2, '"{}" needs a source and a target.', *args 35 | ) 36 | self._validate_kwargs(location, ("work_directory",), **kwargs) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any 44 | ) -> None: 45 | """Execute command.""" 46 | symlink( 47 | system_context, 48 | args[0], 49 | args[1], 50 | work_directory=kwargs.get("work_directory", None), 51 | ) 52 | -------------------------------------------------------------------------------- /examples/system-example.def: -------------------------------------------------------------------------------- 1 | # A simple example server 2 | 3 | based_on type-server 4 | 5 | set_hostname server pretty=Server 6 | set_machine_id cccccccccccccccccccccccccccccccc 7 | 8 | # pkg_amd_cpu 9 | 10 | add_partition 00_esp device=disk0 type=esp minSize=100M maxSize=100M uuid=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 11 | add_partition 10_swap device=disk0 type=swap minSize=1G maxSize=1G uuid=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 12 | add_partition 20_image device=disk0 type=linux-generic minSize=10G maxSize=10G uuid=cccccccccccccccccccccccccccccccc label=image 13 | add_partition 30_var device=disk0 type=var minSize=1G maxSize=2G weight=100 uuid=dddddddddddddddddddddddddddddddd label=var 14 | add_partition 40_home device=disk0 type=home uuid=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee label=home 15 | 16 | # Image setup: 17 | set IMAGE_DEVICE 'PARTUUID=cccccccccccccccccccccccccccccccc' 18 | set IMAGE_FS 'xfs' 19 | set IMAGE_OPTIONS '' 20 | 21 | append /etc/fstab <<<< 22 | PARTUUID=cccccccccccccccccccccccccccccccc /mnt/images xfs defaults,nodev,nosuid,noexec,ro,nofail,noauto 0 1 23 | PARTUUID=dddddddddddddddddddddddddddddddd /var btrfs defaults,nodev,x-initrd.mount,x-systemd.requires=initrd-fs.target 0 1 24 | PARTUUID=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee /home xfs defaults,nodev,nosuid,noexec 0 1 25 | >>>> 26 | 27 | create /etc/crypttab <<<<\ 28 | swap PARTUUID=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb /dev/urandom swap,cipher=aes-xts-plain64,size=256 29 | >>>> mode=0o600 force=True 30 | 31 | append /usr/lib/systemd/network/20-extbr0.network <<<>>> 35 | 36 | export borg_repository 37 | -------------------------------------------------------------------------------- /cleanroom/commands/_teardown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_teardown command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.systemcontext import SystemContext 10 | from cleanroom.location import Location 11 | from cleanroom.printer import debug 12 | 13 | import typing 14 | 15 | 16 | class TeardownCommand(Command): 17 | """The _teardown Command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "_teardown", 23 | help_string="Implicitly run after any other command of a " "system is run.", 24 | file=__file__, 25 | **services, 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any, 39 | ) -> None: 40 | """Execute command.""" 41 | self._run_hooks(system_context, "_teardown") 42 | self._run_hooks(system_context, "testing") 43 | 44 | system_context.pickle() 45 | 46 | self._execute(location, system_context, "_store") 47 | 48 | debug(f'Cleaning up everything in "{system_context.scratch_directory}".') 49 | self._service("btrfs_helper").delete_subvolume_recursive( 50 | system_context.scratch_directory 51 | ) 52 | -------------------------------------------------------------------------------- /cleanroom/commands/chown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """chown command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import chown 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class ChownCommand(Command): 17 | """The chown command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "chown", 23 | syntax="+ [user=] [group=] " "[recursive=False]", 24 | help_string="Chmod a file or files.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_args_at_least( 34 | location, 1, '"{}" takes one or more files.', *args 35 | ) 36 | self._validate_kwargs(location, ("user", "group", "recursive",), **kwargs) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any 44 | ) -> None: 45 | """Execute command.""" 46 | chown( 47 | system_context, 48 | kwargs.get("user", "root"), 49 | kwargs.get("group", "root"), 50 | *args, 51 | recursive=kwargs.get("recursive", False) 52 | ) 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | # Get the long description from the README file 8 | with open(path.join(here, "README.md"), encoding="utf-8") as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | # Package data: 13 | name="cleanroom", 14 | description="Linux system image creator", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | version="0.1", 18 | python_requires=">=3.5", 19 | setup_requires=["pytest-runner",], 20 | tests_require=["pytest",], 21 | url="https://gitlab.con/hunger/cleanroom", 22 | project_urls={ 23 | "Source code": "https://gitlab.com/hunger/cleanroom", 24 | "Code of Conduct": "https://www.python.org/psf/codeofconduct/", 25 | }, 26 | author="Tobias Hunger", 27 | author_email="tobias.hunger@gmail.com", 28 | classifiers=[ 29 | "Development Status :: 4 - Beta" "Intended Audience :: Developers", 30 | "Topic :: Software Development :: Build Tools", 31 | "Environment :: Console", 32 | "License :: OSI Approved :: GPL License", 33 | "Programming Language :: Python :: 3.5", 34 | "Programming Language :: Python :: 3.6", 35 | ], 36 | keywords="Linux stateless immutable install image", 37 | # Contents: 38 | packages=find_packages(exclude=["systems", "examples", "docs", "tests"]), 39 | package_data={"cleanroom": ["commands/*.py"]}, 40 | entry_points={"console_scripts": ["cleanroom=cleanroom.generator.main:run",],}, 41 | ) 42 | -------------------------------------------------------------------------------- /examples/system-dracut.def: -------------------------------------------------------------------------------- 1 | # A simple example server 2 | 3 | based_on type-server 4 | 5 | set CLRM_INITRD_GENERATOR dracut 6 | 7 | set_hostname server pretty=Server 8 | set_machine_id cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd 9 | 10 | # pkg_amd_cpu 11 | 12 | add_partition 00_esp device=disk0 type=esp minSize=100M maxSize=100M uuid=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 13 | add_partition 10_swap device=disk0 type=swap minSize=1G maxSize=1G uuid=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 14 | add_partition 20_image device=disk0 type=linux-generic minSize=10G maxSize=10G uuid=cccccccccccccccccccccccccccccccc label=image 15 | add_partition 30_var device=disk0 type=var minSize=1G maxSize=2G weight=100 uuid=dddddddddddddddddddddddddddddddd label=var 16 | add_partition 40_home device=disk0 type=home uuid=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee label=home 17 | 18 | # Image setup: 19 | set IMAGE_DEVICE 'PARTUUID=cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd' 20 | set IMAGE_FS 'xfs' 21 | set IMAGE_OPTIONS '' 22 | 23 | append /etc/fstab <<<< 24 | PARTUUID=cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd /mnt/images xfs defaults,nodev,nosuid,noexec,ro,nofail,noauto 0 1 25 | PARTUUID=dcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc /var btrfs defaults,nodev,x-initrd.mount,x-systemd.requires=initrd-fs.target 0 1 26 | PARTUUID=ecececececececececececececececec /home xfs defaults,nodev,nosuid,noexec 0 1 27 | >>>> 28 | 29 | create /etc/crypttab <<<<\ 30 | swap PARTUUID=bdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbd /dev/urandom swap,cipher=aes-xts-plain64,size=256 31 | >>>> mode=0o600 force=True 32 | 33 | append /usr/lib/systemd/network/20-extbr0.network <<<>>> 37 | 38 | export borg_repository 39 | -------------------------------------------------------------------------------- /cleanroom/commands/systemd_enable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """systemd_enable command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.systemd import systemd_enable 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class SystemdEnableCommand(Command): 18 | """The systemd_enable command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "systemd_enable", 24 | syntax=" [] [user=False]", 25 | help_string="Enable systemd units.", 26 | file=__file__, 27 | **services 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_args_at_least( 35 | location, 1, '"{}" needs at least one ' "unit to enable.", *args 36 | ) 37 | self._validate_kwargs(location, ("user",), **kwargs) 38 | 39 | def __call__( 40 | self, 41 | location: Location, 42 | system_context: SystemContext, 43 | *args: typing.Any, 44 | **kwargs: typing.Any 45 | ) -> None: 46 | """Execute command.""" 47 | systemd_enable( 48 | system_context, 49 | *args, 50 | systemctl_command=self._binary(Binaries.SYSTEMCTL), 51 | **kwargs 52 | ) 53 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_no_kernel_install.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_no_kernel_install command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class EnsureNoKernelInstallCommand(Command): 16 | """The ensure_no_kernel_install command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "ensure_no_kernel_install", 22 | help_string="Set up system for a read-only /usr partition.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | # Things to update/clean on export: 42 | location.set_description("Remove kernel-install") 43 | self._add_hook( 44 | location, 45 | system_context, 46 | "export", 47 | "remove", 48 | "/usr/lib/kernel", 49 | "/etc/kernel", 50 | "/usr/bin/kernel-install", 51 | recursive=True, 52 | force=True, 53 | ) 54 | -------------------------------------------------------------------------------- /cleanroom/commands/create.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """create command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.file import create_file 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class CreateCommand(Command): 17 | """The create command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "create", 23 | syntax=" [force=True] " 24 | "[mode=0o644] [user=UID/name] [group=GID/name]", 25 | help_string="Create a file with contents.", 26 | file=__file__, 27 | **services 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_args_exact( 35 | location, 36 | 2, 37 | '"{}" takes a file name and the contents ' "to store in the file.", 38 | *args 39 | ) 40 | self._validate_kwargs(location, ("force", "mode", "user", "group"), **kwargs) 41 | 42 | def __call__( 43 | self, 44 | location: Location, 45 | system_context: SystemContext, 46 | *args: typing.Any, 47 | **kwargs: typing.Any 48 | ) -> None: 49 | """Execute command.""" 50 | file_name = args[0] 51 | to_write = args[1].encode("utf-8") 52 | create_file(system_context, file_name, to_write, **kwargs) 53 | -------------------------------------------------------------------------------- /cleanroom/commands/groupmod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """groupmod command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class GroupModCommand(Command): 17 | """The groupmod command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "groupmod", 23 | syntax=" [gid=] [rename=] " 24 | "[password=] [root_directory=]", 25 | help_string="Modify an existing user.", 26 | file=__file__, 27 | **services 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_args_exact(location, 1, '"{}" needs a groupname.', *args) 35 | self._validate_kwargs( 36 | location, ("gid", "rename", "password", "root_directory",), **kwargs 37 | ) 38 | if len(kwargs) == 0: 39 | raise ParseError("groupmod needs something to change.", location=location) 40 | 41 | def __call__( 42 | self, 43 | location: Location, 44 | system_context: SystemContext, 45 | *args: typing.Any, 46 | **kwargs: typing.Any 47 | ) -> None: 48 | """Execute command.""" 49 | self._service("group_helper").groupmod( 50 | args[0], **kwargs, root_directory=system_context.fs_directory 51 | ) 52 | -------------------------------------------------------------------------------- /cleanroom/commands/_store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_store command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.location import Location 9 | from cleanroom.command import Command 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class StoreCommand(Command): 17 | """The _store command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "_store", help_string="Store a system.", file=__file__, **services 23 | ) 24 | 25 | def validate( 26 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 27 | ) -> None: 28 | """Validate the arguments.""" 29 | self._validate_no_arguments(location, *args, **kwargs) 30 | 31 | return None 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | 42 | btrfs_helper = self._service("btrfs_helper") 43 | 44 | btrfs_helper.create_subvolume(system_context.system_storage_directory) 45 | 46 | storage = system_context.system_storage_directory 47 | btrfs_helper.create_snapshot( 48 | system_context.meta_directory, os.path.join(storage, "meta"), read_only=True 49 | ) 50 | btrfs_helper.create_snapshot( 51 | system_context.boot_directory, os.path.join(storage, "boot"), read_only=True 52 | ) 53 | btrfs_helper.create_snapshot( 54 | system_context.fs_directory, os.path.join(storage, "fs"), read_only=True 55 | ) 56 | -------------------------------------------------------------------------------- /cleanroom/commands/crypto_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """crypto_uuid command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class CryptoUuidCommand(Command): 17 | """The crypto_uuid command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "crypto_uuid", 23 | syntax=" ", 24 | help_string="Set the UUID of the crypto partition and the NAME to bind to it.", 25 | file=__file__, 26 | **services, 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_arguments_exact( 34 | location, 2, '"{}" needs a UUID and NAME to set up.', *args 35 | ) 36 | 37 | def __call__( 38 | self, 39 | location: Location, 40 | system_context: SystemContext, 41 | *args: typing.Any, 42 | **kwargs: typing.Any, 43 | ) -> None: 44 | """Execute command.""" 45 | uuid = args[0] 46 | name = args[1] 47 | 48 | cmdline = system_context.substitution("KERNEL_CMDLINE", "") 49 | if "rd.luks.name=" in cmdline: 50 | raise GenerateError("rd.luks.name already set.", location=location) 51 | 52 | system_context.set_or_append_substitution( 53 | "KERNEL_CMDLINE", f"rd.luks.name={uuid}={name} rd.luks.options=discard", 54 | ) 55 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_no_sysusers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_no_sysusers command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class EnsureNoSysusersCommand(Command): 16 | """The ensure_no_sysusers command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "ensure_no_sysusers", 22 | help_string="Set up system for a read-only /usr partition.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | # Things to update/clean on export: 42 | location.set_description("Remove systemd-sysusers") 43 | self._add_hook( 44 | location, 45 | system_context, 46 | "export", 47 | "remove", 48 | "/usr/lib/sysusers.d", 49 | "/usr/bin/systemd-sysusers", 50 | "/usr/lib/systemd/system/sysinit.target.wants/" "systemd-sysusers.service", 51 | "/usr/lib/systemd/system/systemd-sysusers.service", 52 | recursive=True, 53 | force=True, 54 | ) 55 | -------------------------------------------------------------------------------- /cleanroom/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Exceptions used in cleanroom. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from .location import Location 9 | 10 | import typing 11 | 12 | 13 | class CleanRoomError(Exception): 14 | """Base class for all cleanroom Exceptions.""" 15 | 16 | def __init__( 17 | self, 18 | *args: typing.Any, 19 | location: typing.Optional[Location] = None, 20 | original_exception: typing.Optional[Exception] = None, 21 | ) -> None: 22 | """Constructor.""" 23 | super().__init__(*args) 24 | self.location = location 25 | self.original_exception = original_exception 26 | 27 | def set_location(self, location: Location): 28 | self.location = location 29 | 30 | def __str__(self) -> str: 31 | """Stringify exception.""" 32 | prefix = f"Error in {self.location}" if self.location else "Error" 33 | 34 | postfix = "" 35 | if self.original_exception is not None: 36 | if isinstance(self.original_exception, AssertionError): 37 | postfix = "\n Trigger: AssertionError." 38 | else: 39 | postfix = "\n Trigger: " + str(self.original_exception) 40 | 41 | return f"{prefix}: {super().__str__()}{postfix}" 42 | 43 | 44 | class PreflightError(CleanRoomError): 45 | """Error raised in the Preflight Phase.""" 46 | 47 | pass 48 | 49 | 50 | class ParseError(CleanRoomError): 51 | """Error raised while parsing system descriptions.""" 52 | 53 | pass 54 | 55 | 56 | class GenerateError(CleanRoomError): 57 | """Error raised during Generation phase.""" 58 | 59 | pass 60 | 61 | 62 | class SystemNotFoundError(CleanRoomError): 63 | """Error raised when a system could not be found.""" 64 | 65 | pass 66 | -------------------------------------------------------------------------------- /cleanroom/commands/_pacman_keyinit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_pacman_keyinit command 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | from cleanroom.helper.run import run 12 | 13 | import typing 14 | 15 | 16 | class PacstrapCommand(Command): 17 | """The pacstrap command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "_pacman_keyinit", 23 | syntax=" pacman_key= " 24 | "gpg_dir=", 25 | help_string="Enable extra setup for the pacman keyring.", 26 | file=__file__, 27 | **services 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_no_args(location, *args) 35 | self._validate_kwargs(location, ("pacman_key", "gpg_dir"), **kwargs) 36 | self._require_kwargs(location, ("gpg_dir", "gpg_dir"), **kwargs) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any 44 | ) -> None: 45 | """Execute command.""" 46 | 47 | pacman_key_command = kwargs.get("pacman_key", "") 48 | gpg_dir = kwargs.get("gpg_dir", "") 49 | 50 | run( 51 | pacman_key_command, 52 | "--populate", 53 | "archlinux", 54 | "--gpgdir", 55 | gpg_dir, 56 | work_directory=system_context.systems_definition_directory, 57 | ) 58 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_quasselcore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pkg_quasselcore command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class PkgQuasselcoreCommand(Command): 16 | """The pkg_quasselcore command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "pkg_quasselcore", 22 | help_string="Setup quasselcore.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | self._execute( 42 | location, system_context, "pacman", "quassel-core", "postgresql-libs" 43 | ) 44 | self._execute( 45 | location.next_line(), 46 | system_context, 47 | "systemd_harden_unit", 48 | "quassel.service", 49 | ) 50 | self._execute( 51 | location.next_line(), system_context, "systemd_enable", "quassel.service" 52 | ) 53 | 54 | self._execute( 55 | location.next_line(), 56 | system_context, 57 | "net_firewall_open_port", 58 | 4242, 59 | protocol="tcp", 60 | comment="Quassel", 61 | ) 62 | -------------------------------------------------------------------------------- /cleanroom/commands/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """run command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.run import run 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class RunCommand(Command): 18 | """The run command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "run", 24 | syntax=" [] [inside=False] " 25 | "[shell=False] [returncode=0] [stdout=None] " 26 | "[stderr=None]", 27 | help_string="Run a command inside/outside of the " "current system.", 28 | file=__file__, 29 | **services 30 | ) 31 | 32 | def validate( 33 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 34 | ) -> None: 35 | """Validate the arguments.""" 36 | self._validate_args_at_least( 37 | location, 1, '"{}" needs a command to run and ' "optional arguments.", *args 38 | ) 39 | self._validate_kwargs( 40 | location, ("returncode", "inside", "shell", "stderr", "stdout"), **kwargs 41 | ) 42 | 43 | def __call__( 44 | self, 45 | location: Location, 46 | system_context: SystemContext, 47 | *args: typing.Any, 48 | **kwargs: typing.Any 49 | ) -> None: 50 | """Execute command.""" 51 | if kwargs.pop("inside", False): 52 | kwargs["chroot"] = system_context.fs_directory 53 | kwargs["chroot_helper"] = self._binary(Binaries.CHROOT_HELPER) 54 | 55 | run(*list(map(lambda a: str(a), args)), **kwargs) 56 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_no_update_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_no_update_service command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class EnsureNoUpdateServiceCommand(Command): 16 | """The ensure_no_update_service command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "ensure_no_update_service", 22 | help_string="Set up system for a read-only /usr partition.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | # Remove unnecessary systemd-services: 42 | self._execute( 43 | location.next_line(), 44 | system_context, 45 | "remove", 46 | "/usr/lib/systemd/system/systemd-update-done.service", 47 | "/usr/lib/systemd/system/" "system-update-cleanup.service", 48 | "/usr/lib/systemd/system/system-update.target", 49 | "/usr/lib/systemd/system/system-update-pre.target", 50 | "/usr/lib/systemd/system-generators/" "systemd-system-update-generator", 51 | "/usr/lib/systemd/systemd-update-done", 52 | force=True, 53 | ) 54 | -------------------------------------------------------------------------------- /cleanroom/firestarter/mountinstalltarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Mount an exported system 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | import cleanroom.firestarter.tools as tool 11 | from cleanroom.printer import verbose 12 | 13 | import os 14 | from shlex import split 15 | import typing 16 | 17 | 18 | def _execution(efi: str, rootfs: str, *, command: str) -> int: 19 | to_exec = command or '/usr/bin/bash -c "read -n1 -s"' 20 | prompt = "" if command else "<<< Press any key to continue >>>" 21 | 22 | env = os.environ 23 | env["EFI_MOUNT"] = efi 24 | env["ROOT_MOUNT"] = rootfs 25 | 26 | verbose(f"Running {command}.") 27 | 28 | print(f'EFI partition is mounted at "{efi}".') 29 | print(f'Root partition is mounted at "{rootfs}".') 30 | 31 | if prompt: 32 | print(prompt) 33 | 34 | return tool.run(*split(to_exec), env=env).returncode 35 | 36 | 37 | class MountInstallTarget(InstallTarget): 38 | def __init__(self) -> None: 39 | super().__init__( 40 | "mount", 41 | "RO mounts EFI and root partition till " 42 | "the given command is done executing.", 43 | ) 44 | 45 | def __call__( 46 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 47 | ) -> int: 48 | return tool.execute_with_system_mounted( 49 | lambda e, r: _execution(e, r, command=parse_result.command), 50 | image_file=image_file, 51 | tmp_dir=tmp_dir, 52 | ) 53 | 54 | def setup_subparser(self, subparser: typing.Any) -> None: 55 | subparser.add_argument( 56 | "--command", 57 | action="store", 58 | nargs="?", 59 | default="", 60 | help="Command to run once mounted " "[default: wait for keypress].", 61 | ) 62 | -------------------------------------------------------------------------------- /cleanroom/commands/_strip_documentation_hook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_strip_documentation_hook command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.location import Location 9 | from cleanroom.printer import debug 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os.path 13 | import typing 14 | 15 | 16 | class StripDocumentationHookCommand(Command): 17 | """The strip_documentation Command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "_strip_documentation_hook", 23 | help_string="Strip away documentation files (hook).", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any 40 | ) -> None: 41 | """Execute command.""" 42 | location.set_description("Strip documentation files") 43 | to_remove = ["/usr/share/doc/*", "/usr/share/gtk-doc/html", "/usr/share/help/*"] 44 | if not os.path.exists(system_context.file_name("/usr/bin/man")): 45 | debug("No /usr/bin/man: Removing man pages.") 46 | to_remove += ["/usr/share/man/*"] 47 | if not os.path.exists(system_context.file_name("/usr/bin/info")): 48 | debug("No /usr/bin/info: Removing info pages.") 49 | to_remove += ["/usr/share/info/*"] 50 | self._execute( 51 | location, system_context, "remove", *to_remove, recursive=True, force=True 52 | ) 53 | -------------------------------------------------------------------------------- /cleanroom/commands/move.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """move command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.helper.file import move 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class MoveCommand(Command): 18 | """The move command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "move", 24 | syntax=" [] " 25 | " [ignore_missing_sources=False]" 26 | " [from_outside=False] [to_outside=False] " 27 | "[force=False]", 28 | help_string="Move file or directory.", 29 | file=__file__, 30 | **services 31 | ) 32 | 33 | def validate( 34 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 35 | ) -> None: 36 | """Validate the arguments.""" 37 | self._validate_args_at_least( 38 | location, 2, '"{}" needs at least one ' "source and a destination.", *args 39 | ) 40 | self._validate_kwargs( 41 | location, 42 | ("from_outside", "to_outside", "ignore_missing_sources", "force"), 43 | **kwargs 44 | ) 45 | if kwargs.get("from_outside", False) and kwargs.get("to_outside", False): 46 | raise ParseError( 47 | "You can not move a file from_outside to_outside.", location=location 48 | ) 49 | 50 | def __call__( 51 | self, 52 | location: Location, 53 | system_context: SystemContext, 54 | *args: typing.Any, 55 | **kwargs: typing.Any 56 | ) -> None: 57 | """Execute command.""" 58 | move(system_context, *args, **kwargs) 59 | -------------------------------------------------------------------------------- /cleanroom/commands/useradd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """useradd command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.exceptions import ParseError 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class UseraddCommand(Command): 16 | """The useradd command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "useradd", 22 | syntax=" [comment=] [home=] " 23 | "[gid=] [uid=] [groups=,] " 24 | "[lock=False] [password=] " 25 | "[shell=] [expire=]", 26 | help_string="Modify an existing user.", 27 | file=__file__, 28 | **services 29 | ) 30 | 31 | def validate( 32 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 33 | ) -> None: 34 | """Validate the arguments.""" 35 | self._validate_args_exact(location, 1, '"{}" needs a username.', *args) 36 | if len(kwargs) == 0: 37 | raise ParseError("useradd needs keyword arguments", location=location) 38 | 39 | lock = kwargs.get("lock", None) 40 | if lock not in (True, None, False): 41 | raise ParseError( 42 | '"lock" must be either True, False or None.', location=location 43 | ) 44 | 45 | def __call__( 46 | self, 47 | location: Location, 48 | system_context: SystemContext, 49 | *args: typing.Any, 50 | **kwargs: typing.Any 51 | ) -> None: 52 | """Execute command.""" 53 | self._service("user_helper").useradd( 54 | args[0], **kwargs, root_directory=system_context.fs_directory 55 | ) 56 | -------------------------------------------------------------------------------- /cleanroom/commands/_depmod_all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_depmod_all command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import os 14 | import typing 15 | 16 | 17 | class DepmodAllCommand(Command): 18 | """The depmod_all command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "_depmod_all", 24 | help_string="Make sure all module dependecies are up to date.", 25 | file=__file__, 26 | **services, 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_no_arguments(location, *args, **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any, 41 | ) -> None: 42 | """Execute command.""" 43 | modules = system_context.file_name("/usr/lib/modules") 44 | if not os.path.isdir(modules): 45 | return # No kernel installed, nothing to do. 46 | 47 | for kver in [ 48 | f for f in os.listdir(modules) if os.path.isdir(os.path.join(modules, f)) 49 | ]: 50 | location.set_description(f"Run depmod for kernel version {kver}...") 51 | self._execute( 52 | location, 53 | system_context, 54 | "run", 55 | self._binary(Binaries.DEPMOD), 56 | "-a", 57 | "-b", 58 | system_context.fs_directory, 59 | kver, 60 | ) 61 | -------------------------------------------------------------------------------- /cleanroom/commands/net_firewall_open_port.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """net_firewall_open_port command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.helper.archlinux.iptables import open_port 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class NetFirewallOpenPortCommand(Command): 18 | """The net_firewall_open_port command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "net_firewall_open_port", 24 | syntax=" [protocol=(tcp|udp)] [comment=]", 25 | help_string="Open a port in the firewall.", 26 | file=__file__, 27 | **services, 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_args_exact(location, 1, '"{}" needs a port to open.', *args) 35 | self._validate_kwargs(location, ("protocol", "comment"), **kwargs) 36 | 37 | protocol = kwargs.get("protocol", "tcp") 38 | if protocol != "tcp" and protocol != "udp": 39 | raise ParseError( 40 | f'"{self.name}" only supports protocols "tcp" and "udp".', 41 | location=location, 42 | ) 43 | 44 | def __call__( 45 | self, 46 | location: Location, 47 | system_context: SystemContext, 48 | *args: typing.Any, 49 | **kwargs: typing.Any, 50 | ) -> None: 51 | """Execute command.""" 52 | protocol = kwargs.get("protocol", "tcp") 53 | comment = kwargs.get("comment", "") 54 | open_port(system_context, args[0], protocol=protocol, comment=comment) 55 | -------------------------------------------------------------------------------- /cleanroom/commands/copy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """copy command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.helper.file import copy 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class CopyCommand(Command): 18 | """The copy command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "copy", 24 | syntax="+ [ignore_missing=False] " 25 | "[from_outside=True] [to_outside=True] " 26 | "[recursive=False] [force=False]", 27 | help_string="Copy a file within the system.", 28 | file=__file__, 29 | **services, 30 | ) 31 | 32 | def validate( 33 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 34 | ) -> None: 35 | """Validate the arguments.""" 36 | self._validate_args_at_least( 37 | location, 2, '"{}" needs one or more sources and a ' "destination", *args 38 | ) 39 | self._validate_kwargs( 40 | location, 41 | ("from_outside", "to_outside", "ignore_missing", "recursive", "force"), 42 | **kwargs, 43 | ) 44 | 45 | if kwargs.get("from_outside", False) and kwargs.get("to_outside", False): 46 | raise ParseError( 47 | f'You can not "{self.name}" a file from_outside to_outside.', 48 | location=location, 49 | ) 50 | 51 | def __call__( 52 | self, 53 | location: Location, 54 | system_context: SystemContext, 55 | *args: typing.Any, 56 | **kwargs: typing.Any, 57 | ) -> None: 58 | """Execute command.""" 59 | copy(system_context, *args, **kwargs) 60 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_amd_cpu.py: -------------------------------------------------------------------------------- 1 | """pkg_amd_cpu command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.helper.file import create_file 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class PkgAmdCpuCommand(Command): 17 | """The pkg_amd_cpu command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "pkg_amd_cpu", 23 | help_string="Install everything for amd CPU.", 24 | file=__file__, 25 | **services, 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any, 40 | ) -> None: 41 | """Execute command.""" 42 | 43 | # Nested virtualization: 44 | create_file( 45 | system_context, 46 | "/etc/modprobe.d/kvm_amd.conf", 47 | "options kvm_amd nested=1".encode("utf-8"), 48 | ) 49 | 50 | # AMD ucode: 51 | location.set_description("Install amd-ucode") 52 | self._execute(location, system_context, "pacman", "amd-ucode") 53 | 54 | initrd_parts = os.path.join(system_context.boot_directory, "initrd-parts") 55 | os.makedirs(initrd_parts, exist_ok=True) 56 | self._execute( 57 | location, 58 | system_context, 59 | "move", 60 | "/boot/amd-ucode.img", 61 | os.path.join(initrd_parts, "00-amd-ucode"), 62 | to_outside=True, 63 | ) 64 | -------------------------------------------------------------------------------- /cleanroom/commands/swupd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """swupd command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.exceptions import GenerateError, ParseError 11 | from cleanroom.location import Location 12 | from cleanroom.helper.run import run 13 | from cleanroom.systemcontext import SystemContext 14 | 15 | import typing 16 | 17 | 18 | class swupdCommand(Command): 19 | """The swupd command.""" 20 | 21 | def __init__(self, **services: typing.Any) -> None: 22 | """Constructor.""" 23 | super().__init__( 24 | "swupd", 25 | target_distribution="clr", 26 | syntax="", 27 | help_string="Run swupd to install .", 28 | file=__file__, 29 | **services, 30 | ) 31 | 32 | def validate( 33 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 34 | ) -> None: 35 | """Validate the arguments.""" 36 | if not self._binary(Binaries.SWUPD): 37 | raise ParseError("No swupd binary was found.") 38 | 39 | self._validate_arguments_at_least( 40 | location, 1, '"{}" needs at least one package or group to install.', *args 41 | ) 42 | 43 | def __call__( 44 | self, 45 | location: Location, 46 | system_context: SystemContext, 47 | *args: typing.Any, 48 | **kwargs: typing.Any, 49 | ) -> None: 50 | """Execute command.""" 51 | # Validate: 52 | if system_context.substitution("CLRM_PACKAGE_TYPE", "") != "swupd": 53 | raise GenerateError( 54 | "Trying to run swupd when other package type has been initialized before." 55 | ) 56 | 57 | run( 58 | self._binary(Binaries.SWUPD), 59 | "bundle-add", 60 | f"--path={system_context.fs_directory}", 61 | *args, 62 | ) 63 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_nvidia_gpu.py: -------------------------------------------------------------------------------- 1 | """pkg_nvidia_gpu command. 2 | 3 | @author: Paul Hunnisett 4 | """ 5 | 6 | from cleanroom.command import Command 7 | from cleanroom.location import Location 8 | from cleanroom.systemcontext import SystemContext 9 | 10 | import typing 11 | 12 | 13 | class PkgNvidiaGpuCommand(Command): 14 | """The pkg_nvidia_gpu command.""" 15 | 16 | def __init__(self, **services: typing.Any) -> None: 17 | """Constructor.""" 18 | super().__init__( 19 | "pkg_nvidia_gpu", 20 | help_string="Set up NVidia GPU.", 21 | file=__file__, 22 | **services 23 | ) 24 | 25 | def validate( 26 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 27 | ) -> None: 28 | """Validate the arguments.""" 29 | self._validate_no_arguments(location, *args, **kwargs) 30 | 31 | def __call__( 32 | self, 33 | location: Location, 34 | system_context: SystemContext, 35 | *args: typing.Any, 36 | **kwargs: typing.Any 37 | ) -> None: 38 | """Execute command.""" 39 | 40 | # Set some kernel parameters for: 41 | system_context.set_or_append_substitution( 42 | "KERNEL_CMDLINE", "nvidia-drm.modeset=1 nouveau.blacklist=1" 43 | ) 44 | 45 | self._execute( 46 | location, 47 | system_context, 48 | "pacman", 49 | "nvidia", 50 | "nvidia-settings", 51 | "nvidia-utils", 52 | "opencl-nvidia", 53 | "libvdpau", 54 | "lib32-libvdpau", 55 | "lib32-nvidia-utils", 56 | "lib32-opencl-nvidia", 57 | "vdpauinfo", 58 | "mesa", 59 | "mesa-demos", 60 | ) 61 | 62 | self._execute( 63 | location.next_line(), 64 | system_context, 65 | "create", 66 | "/etc/modprobe.d/nouveau-blacklist.conf", 67 | "blacklist nouveau", 68 | ) 69 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_fonts.py: -------------------------------------------------------------------------------- 1 | """pkg_fonts command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.helper.file import symlink 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class PkgFontsCommand(Command): 16 | """The pkg_fonts command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "pkg_fonts", 22 | help_string="Set up some extra fonts.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | self._execute( 42 | location, 43 | system_context, 44 | "pacman", 45 | "adobe-source-code-pro-fonts", 46 | "ttf-bitstream-vera", 47 | "ttf-dejavu", 48 | "gentium-plus-font", 49 | "ttf-inconsolata", 50 | "noto-fonts", 51 | "noto-fonts-cjk", 52 | "noto-fonts-emoji", 53 | "noto-fonts-extra", 54 | "ttf-roboto", 55 | "ttf-fira-code", 56 | ) 57 | 58 | symlink( 59 | system_context, 60 | "../conf.avail.d/11-lcdfilter-default.conf", 61 | "11-lcdfilter-default.conf", 62 | work_directory="/etc/fonts/conf.d", 63 | ) 64 | symlink( 65 | system_context, 66 | "../conf.avail.d/10-subpixel-rgb.conf", 67 | "10-subpixel-rgb.conf", 68 | work_directory="/etc/fonts/conf.d", 69 | ) 70 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Test for the exceptions of cleanroom. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import os 9 | import sys 10 | 11 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 12 | 13 | import cleanroom.exceptions as ex 14 | import cleanroom.location as location 15 | 16 | 17 | def test_base_exceptions() -> None: 18 | """Test the base exception class.""" 19 | e = ex.CleanRoomError("message") 20 | assert str(e) == "Error: message" 21 | 22 | 23 | def test_base_exceptions_multi_args() -> None: 24 | """Test the base exception class.""" 25 | e = ex.CleanRoomError("message", "something") 26 | assert str(e) == "Error: ('message', 'something')" 27 | 28 | 29 | def test_base_exceptions_with_location() -> None: 30 | """Test the base exception class with file.""" 31 | loc = location.Location(file_name="/foo/bar") 32 | e = ex.CleanRoomError("message", location=loc) 33 | assert str(e) == "Error in /foo/bar: message" 34 | 35 | 36 | def test_base_exceptions_with_file_and_line() -> None: 37 | """Test the base exception class with file and line.""" 38 | loc = location.Location(file_name="/foo/bar", line_number=42) 39 | e = ex.CleanRoomError("message", location=loc) 40 | assert str(e) == "Error in /foo/bar:42: message" 41 | 42 | 43 | def test_preflight() -> None: 44 | """Test preflight exception.""" 45 | e = ex.PreflightError("Something went wrong") 46 | assert str(e) == "Error: Something went wrong" 47 | 48 | 49 | def test_generate() -> None: 50 | """Test prepare exception.""" 51 | e = ex.GenerateError("Something went wrong") 52 | assert str(e) == "Error: Something went wrong" 53 | 54 | 55 | def test_system_not_found() -> None: 56 | """Test system not found exception.""" 57 | e = ex.SystemNotFoundError("Something went wrong") 58 | assert str(e) == "Error: Something went wrong" 59 | 60 | 61 | def test_parse() -> None: 62 | """Test parse exception.""" 63 | e = ex.ParseError("Something went wrong") 64 | assert str(e) == "Error: Something went wrong" 65 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_ldconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_ldconfig command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class EnsureLdconfigCommand(Command): 17 | """The ensure_ldconfig command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "ensure_ldconfig", 23 | help_string="Ensure that ldconfig is run.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any 40 | ) -> None: 41 | """Execute command.""" 42 | assert os.path.exists(system_context.file_name("/usr/bin/ldconfig")) 43 | 44 | location.set_description("Run ldconfig") 45 | self._add_hook( 46 | location, 47 | system_context, 48 | "export", 49 | "run", 50 | "/usr/bin/ldconfig", 51 | "-X", 52 | inside=True, 53 | ) 54 | location.set_description("Remove ldconfig data") 55 | # self._add_hook(location, system_context, 56 | # 'export', 'remove', '/usr/bin/ldconfig') 57 | location.set_description("Remove ldconfig related services") 58 | self._add_hook( 59 | location, 60 | system_context, 61 | "export", 62 | "remove", 63 | "/usr/lib/systemd/system/*/ldconfig.service", 64 | "/usr/lib/systemd/system/ldconfig.service", 65 | force=True, 66 | ) 67 | -------------------------------------------------------------------------------- /cleanroom/commands/net_firewall_enable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """net_firewall_enable command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.helper.archlinux.iptables import firewall_type 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | import os 15 | 16 | 17 | class NetFirewallEnableCommand(Command): 18 | """The net_firewall_enable command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "net_firewall_enable", 24 | help_string="Enable previously configured firewall.", 25 | file=__file__, 26 | **services, 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_no_arguments(location, *args, **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any 41 | ) -> None: 42 | """Execute command.""" 43 | assert firewall_type(system_context) == "iptables" 44 | location.set_description("Enable firewall") 45 | to_enable: typing.List[str] = [] 46 | if os.path.exists( 47 | system_context.file_name("/usr/lib/systemd/system/iptables.service") 48 | ): 49 | to_enable.append("iptables.service") 50 | if os.path.exists( 51 | system_context.file_name("/usr/lib/systemd/system/ip6tables.service") 52 | ): 53 | to_enable.append("ip6tables.service") 54 | if os.path.exists( 55 | system_context.file_name("/usr/lib/systemd/system/iptables-restore.service") 56 | ): 57 | to_enable.append("iptables-restore.service") 58 | 59 | self._execute( 60 | location, system_context, "systemd_enable", *to_enable, 61 | ) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cleanroom 2 | 3 | Cleanroom makes it easy and fast to do fresh installations of whole 4 | fleets of machines (bare metal, VMs or containers). 5 | 6 | A set of system descriptions is used to describe the machines to be 7 | installed. Cleanroom will use this description and files downloaded 8 | from Linux Distributions to do the actual installations. 9 | 10 | By enabling easy and fast installations, Cleanroom enables the use 11 | of immutable and stateless systems as it becomes feasible to keep 12 | such machines up-to-date by simply regenerating a fresh image and 13 | booting into it. 14 | 15 | Up-to-date code can be found at: 16 | 17 | https://github.com/cleanroom-team/cleanroom 18 | 19 | 20 | ## Installation 21 | 22 | Just use the code straight from the repository:-) 23 | 24 | ### Optional Build Container 25 | 26 | If you do not want your systems to be built in your normal OS setup, 27 | you can create an optional build container for Cleanroom. 28 | 29 | ``` 30 | pacstrap clrm-archlinux \ 31 | arch-install-scripts \ 32 | binutils borg btrfs-progs \ 33 | cpio \ 34 | devtools dosfstools \ 35 | lsof \ 36 | mtools \ 37 | pacman python-pyparsing \ 38 | qemu \ 39 | sbsigntools \ 40 | squashfs-tools \ 41 | tar 42 | ``` 43 | 44 | should get you started. 45 | 46 | Use 47 | 48 | ``` 49 | build_container \ 50 | --build-container=clrm-archlinux \ 51 | --systems-directory=/SYSTEMS/DIRECTORY \ 52 | --work-directory=/WORK/DIR \ 53 | --repository-base-directory=/REPOSITORY/BASE \ 54 | clrm --verbose --verbose --verbose --verbose \ 55 | --clear-scratch-directory \ 56 | SYSTEM_NAME 57 | ``` 58 | 59 | to build inside this container. 60 | 61 | ## Tests 62 | 63 | Use ```pytest tests``` in the top level directory to run all tests. 64 | 65 | ## Contributors 66 | 67 | * Tobias Hunger <tobias.hunger@gmail.com> 68 | 69 | ## Code of Conduct 70 | 71 | Everybody is expected to follow the Python Community Code of Conduct 72 | https://www.python.org/psf/codeofconduct/ 73 | 74 | ## License 75 | 76 | All files in cleanroom are under GPL v3 (or later). 77 | 78 | See LICENSE.md for the full license text. 79 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_no_unused_shell_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_no_unused_shell_files command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class EnsureNoUnusedShellFilesCommand(Command): 16 | """The ensure_no_unused_shell_files command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "ensure_no_unused_shell_files", 22 | help_string="Clean out files for shells that are not installed.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | 42 | # Shell cleanup: 43 | location.set_description("Clear shell files") 44 | self._add_hook( 45 | location, 46 | system_context, 47 | "_teardown", 48 | "run", 49 | "test", 50 | "-x", 51 | "/usr/bin/zsh", 52 | "&&", 53 | "rm", 54 | "-rf", 55 | "/usr/share/zsh", 56 | shell=True, 57 | returncode=None, 58 | ) 59 | self._add_hook( 60 | location, 61 | system_context, 62 | "_teardown", 63 | "run", 64 | "test", 65 | "-x", 66 | "/usr/bin/bash", 67 | "&&", 68 | "rm", 69 | "-rf", 70 | "/usr/share/bash-completion", 71 | shell=True, 72 | returncode=None, 73 | ) 74 | -------------------------------------------------------------------------------- /cleanroom/commands/set_timezone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """set_timezone command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError 10 | from cleanroom.helper.file import exists 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class SetTimezoneCommand(Command): 18 | """The set_timezone command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "set_timezone", 24 | syntax="", 25 | help_string="Set up the timezone for a system.", 26 | file=__file__, 27 | **services, 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_arguments_exact( 35 | location, 1, '"{}" needs a timezone to set up.', *args, **kwargs 36 | ) 37 | 38 | def __call__( 39 | self, 40 | location: Location, 41 | system_context: SystemContext, 42 | *args: typing.Any, 43 | **kwargs: typing.Any, 44 | ) -> None: 45 | """Execute command.""" 46 | etc = "/etc" 47 | localtime = "localtime" 48 | etc_localtime = etc + "/" + localtime 49 | 50 | timezone = args[0] 51 | full_timezone = "../usr/share/zoneinfo/" + timezone 52 | if not exists(system_context, full_timezone, work_directory=etc): 53 | raise GenerateError( 54 | f'Timezone "{timezone}" not found when trying to set timezone.', 55 | location=location, 56 | ) 57 | 58 | self._execute(location, system_context, "remove", etc_localtime) 59 | self._execute( 60 | location.next_line(), 61 | system_context, 62 | "symlink", 63 | full_timezone, 64 | localtime, 65 | work_directory="/etc", 66 | ) 67 | -------------------------------------------------------------------------------- /cleanroom/commands/ensure_hwdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ensure_hwdb command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class EnsureHwdbCommand(Command): 17 | """The ensure_hwdb command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "ensure_hwdb", 23 | help_string="Make sure hwdb is installed.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any 40 | ) -> None: 41 | """Execute command.""" 42 | assert os.path.exists(system_context.file_name("/usr/bin/systemd-hwdb")) 43 | 44 | location.set_description("Update HWDB") 45 | self._add_hook( 46 | location, 47 | system_context, 48 | "export", 49 | "run", 50 | "/usr/bin/systemd-hwdb", 51 | "--usr", 52 | "update", 53 | inside=True, 54 | ) 55 | location.set_description("Remove HWDB data") 56 | self._add_hook( 57 | location, system_context, "export", "remove", "/usr/bin/systemd-hwdb" 58 | ) 59 | location.set_description("Remove HWDB related services") 60 | self._add_hook( 61 | location, 62 | system_context, 63 | "export", 64 | "remove", 65 | "/usr/lib/systemd/system/*/" "systemd-hwdb-update.service", 66 | "/usr/lib/systemd/system/" "systemd-hwdb-update.service", 67 | force=True, 68 | ) 69 | -------------------------------------------------------------------------------- /cleanroom/commands/firejail_apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """firejail_apps command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError, ParseError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import os.path 14 | import typing 15 | 16 | 17 | class FirejailAppsConfigureCommand(Command): 18 | """The firejail_apps command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "firejail_apps", 24 | syntax="+", 25 | help_string="Firejail applications.", 26 | file=__file__, 27 | **services, 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | if not args: 35 | raise ParseError( 36 | f'"{self.name}" does need at least one application.', location=location, 37 | ) 38 | self._validate_arguments_at_least( 39 | location, 1, '"{}" needs at least one application.', *args 40 | ) 41 | 42 | def __call__( 43 | self, 44 | location: Location, 45 | system_context: SystemContext, 46 | *args: typing.Any, 47 | **kwargs: typing.Any, 48 | ) -> None: 49 | """Execute command.""" 50 | for a in args: 51 | location.set_description(f"Processing application {a}.") 52 | desktop_file = f"/usr/share/applications/{a}.desktop" 53 | if not os.path.exists(system_context.file_name(desktop_file)): 54 | raise GenerateError( 55 | f'Desktop file "{desktop_file}" not found.', location=location, 56 | ) 57 | self._execute( 58 | location.next_line(), 59 | system_context, 60 | "sed", 61 | "/^Exec=.*$$/ s!^Exec=!Exec=/usr/bin/firejail !", 62 | desktop_file, 63 | ) 64 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_kernel.py: -------------------------------------------------------------------------------- 1 | """pkg_kernel command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.location import Location 9 | from cleanroom.systemcontext import SystemContext 10 | 11 | import typing 12 | import os.path 13 | 14 | 15 | class PkgKernelCommand(Command): 16 | """The pkg_kernel command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "pkg_kernel", 22 | syntax_string="[variant=(lts|hardened|DEFAULT)]", 23 | help_string="Set up a Kernel. If no variant is given, then the default kernel is installed.", 24 | file=__file__, 25 | **services, 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate the arguments.""" 32 | self._validate_no_args(location, *args) 33 | self._validate_kwargs(location, ("variant",), **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any, 41 | ) -> None: 42 | """Execute command.""" 43 | 44 | kernel = "linux" 45 | variant = kwargs.get("variant", "") 46 | if variant: 47 | kernel = f"{kernel}-{variant}" 48 | 49 | self._execute( 50 | location, 51 | system_context, 52 | "pacman", 53 | "--assume-installed", 54 | "initramfs", 55 | kernel, 56 | ) 57 | 58 | vmlinuz = os.path.join(system_context.boot_directory, "vmlinuz") 59 | 60 | # New style linux packages that put vmlinuz into /usr/lib/modules: 61 | self._execute( 62 | location.next_line(), 63 | system_context, 64 | "move", 65 | "/usr/lib/modules/*/vmlinuz", 66 | vmlinuz, 67 | to_outside=True, 68 | ignore_missing_sources=True, 69 | ) 70 | 71 | assert os.path.isfile(vmlinuz) 72 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_tmux.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pkg_tmux command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import textwrap 13 | import typing 14 | 15 | 16 | class PkgTmuxCommand(Command): 17 | """The pkg_tmux command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "pkg_tmux", help_string="Setup tmux.", file=__file__, **services 23 | ) 24 | 25 | def validate( 26 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 27 | ) -> None: 28 | """Validate the arguments.""" 29 | self._validate_no_arguments(location, *args, **kwargs) 30 | 31 | def __call__( 32 | self, 33 | location: Location, 34 | system_context: SystemContext, 35 | *args: typing.Any, 36 | **kwargs: typing.Any 37 | ) -> None: 38 | """Execute command.""" 39 | self._execute(location, system_context, "pacman", "tmux") 40 | 41 | self._execute( 42 | location.next_line(), 43 | system_context, 44 | "create", 45 | "/root/.tmux.conf", 46 | textwrap.dedent( 47 | """\ 48 | # Set activation key to ctrl-A: 49 | set-option -g prefix C-a 50 | 51 | # Rebind splitting to | and -: 52 | unbind % 53 | bind | split-window -h 54 | bind - split-window -v 55 | 56 | # Last window on C-a C-a: 57 | bind-key C-a last-window 58 | 59 | # Highlight active window 60 | set-window-option -g window-status-current-bg red 61 | 62 | # Set window notifications 63 | setw -g monitor-activity on 64 | set -g visual-activity on 65 | """ 66 | ), 67 | mode=0o600, 68 | ) 69 | -------------------------------------------------------------------------------- /cleanroom/commands/strip_development_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """strip_development_files command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | import os 15 | 16 | 17 | class StripDevelopmentFilesCommand(Command): 18 | """The strip_development_files Command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "strip_development_files", 24 | help_string="Strip away development files.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate arguments.""" 33 | self._validate_no_arguments(location, *args, **kwargs) 34 | 35 | def __call__( 36 | self, 37 | location: Location, 38 | system_context: SystemContext, 39 | *args: typing.Any, 40 | **kwargs: typing.Any 41 | ) -> None: 42 | """Execute command.""" 43 | location.set_description("Strip development files") 44 | self._add_hook( 45 | location, 46 | system_context, 47 | "export", 48 | "remove", 49 | "/usr/include/*", 50 | "/usr/src/*", 51 | "/usr/share/pkgconfig/*", 52 | "/usr/lib/pkgconfig/*", 53 | "/usr/share/aclocal/*", 54 | "/usr/lib/cmake/*", 55 | "/usr/share/gir-1.0/*", 56 | recursive=True, 57 | force=True, 58 | ) 59 | 60 | # Remove .so symlinks: 61 | directory = system_context.file_name("/usr/lib") 62 | for f in os.listdir(directory): 63 | fullname = os.path.join(directory, f) 64 | if fullname.endswith("/libnss_files.so"): 65 | continue 66 | if fullname.endswith(".a") or ( 67 | fullname.endswith(".so") and os.path.islink(fullname) 68 | ): 69 | os.unlink(fullname) 70 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_xorg.py: -------------------------------------------------------------------------------- 1 | """pkg_xorg command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.helper.file import chmod, chown, copy, create_file 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import textwrap 13 | import typing 14 | 15 | 16 | class PkgXorgCommand(Command): 17 | """The pkg_xorg command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "pkg_xorg", help_string="Set up Xorg.", file=__file__, **services 23 | ) 24 | 25 | def validate( 26 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 27 | ) -> None: 28 | """Validate the arguments.""" 29 | self._validate_no_arguments(location, *args, **kwargs) 30 | 31 | def __call__( 32 | self, 33 | location: Location, 34 | system_context: SystemContext, 35 | *args: typing.Any, 36 | **kwargs: typing.Any 37 | ) -> None: 38 | """Execute command.""" 39 | self._execute( 40 | location, system_context, "pacman", "xorg-server", "xorg-server-xwayland" 41 | ) 42 | 43 | # Copy snippets from systems config folder: 44 | copy( 45 | system_context, 46 | self._config_directory(system_context) + "/*", 47 | "/etc/X11/xorg.conf.d", 48 | from_outside=True, 49 | recursive=True, 50 | ) 51 | chown(system_context, 0, 0, "/etc/X11/xorg.conf.d/*") 52 | chmod(system_context, 0o644, "/etc/X11/xorg.conf.d/*") 53 | 54 | create_file( 55 | system_context, 56 | "/etc/X11/xinit/xinitrc.d/99-access-to-user.sh", 57 | textwrap.dedent( 58 | """\ 59 | #!/usr/bin/bash 60 | 61 | # Allow local access for the user: 62 | xhost "+local:$$USER" 63 | """ 64 | ).encode("utf-8"), 65 | mode=0o755, 66 | ) 67 | 68 | # Install some extra fonts: 69 | self._execute(location.next_line(), system_context, "pkg_fonts") 70 | -------------------------------------------------------------------------------- /tests/test_cmd_run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Test for the run of cleanroom. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import pytest # type: ignore 9 | 10 | import os 11 | import sys 12 | 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 14 | 15 | from cleanroom.commandmanager import _process_args, _process_kwargs 16 | 17 | 18 | @pytest.mark.parametrize( 19 | ("test_input", "expected"), 20 | [ 21 | pytest.param(("test", "1, 2, 3"), ("test", "1, 2, 3"), id="basic"), 22 | pytest.param( 23 | ("test", 1, True, False, None), 24 | ("test", 1, True, False, None), 25 | id="special inputs", 26 | ), 27 | pytest.param( 28 | ("${ROOT_DIR}/etc/testfile",), 29 | ("/some/place/etc/testfile",), 30 | id="substitution", 31 | ), 32 | ], 33 | ) 34 | def test_cmd_run_map_args(system_context, test_input, expected): 35 | """Test map_base.""" 36 | system_context.set_substitution("TEST", "") 37 | system_context.set_substitution("ROOT_DIR", "/some/place") 38 | 39 | result = _process_args(system_context, *test_input) 40 | assert result == expected 41 | 42 | 43 | @pytest.mark.parametrize( 44 | ("test_input", "expected"), 45 | [ 46 | pytest.param( 47 | {"test": "fortytwo", "foo": "bar"}, 48 | {"test": "fortytwo", "foo": "bar"}, 49 | id="basic", 50 | ), 51 | pytest.param( 52 | {"test": True, "foo": False, "nothing": None, "int": 42}, 53 | {"test": True, "foo": False, "nothing": None, "int": 42}, 54 | id="special inputs", 55 | ), 56 | pytest.param( 57 | {"path": "${ROOT_DIR}/some/file", "int": 42}, 58 | {"path": "/some/place/some/file", "int": 42}, 59 | id="substitution", 60 | ), 61 | ], 62 | ) 63 | def test_cmd_run_map_kwargs(system_context, test_input, expected): 64 | """Test map_base.""" 65 | system_context.set_substitution("TEST", "") 66 | system_context.set_substitution("ROOT_DIR", "/some/place") 67 | 68 | result = _process_kwargs(system_context, **test_input) 69 | assert result == expected 70 | -------------------------------------------------------------------------------- /cleanroom/commands/tar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tar command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | from cleanroom.binarymanager import Binaries 8 | from cleanroom.command import Command 9 | from cleanroom.helper.run import run 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import os.path 14 | import typing 15 | 16 | 17 | class TarCommand(Command): 18 | """The tar command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "tar", 24 | syntax=" " 25 | "[to_outside=False] [compress=False] " 26 | "[work_directory=]", 27 | help_string="Create a tarball.", 28 | file=__file__, 29 | **services 30 | ) 31 | 32 | def validate( 33 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 34 | ) -> None: 35 | """Validate the arguments.""" 36 | self._validate_args_exact( 37 | location, 2, '"{}" needs a source and a target.', *args 38 | ) 39 | self._validate_kwargs( 40 | location, ("to_outside", "compress", "work_directory",), **kwargs 41 | ) 42 | 43 | def __call__( 44 | self, 45 | location: Location, 46 | system_context: SystemContext, 47 | *args: typing.Any, 48 | **kwargs: typing.Any 49 | ) -> None: 50 | """Execute command.""" 51 | work_directory = kwargs.get("work_directory", "/") 52 | source = system_context.file_name(os.path.join(work_directory, args[0])) 53 | 54 | to_outside = kwargs.get("to_outside", False) 55 | 56 | target = ( 57 | args[1] 58 | if to_outside 59 | else system_context.file_name(os.path.join(work_directory, args[1])) 60 | ) 61 | assert os.path.isabs(target) 62 | 63 | arguments = ["-cz"] if kwargs.get("compress", False) else ["-c"] 64 | run( 65 | self._binary(Binaries.TAR), 66 | *arguments, 67 | "-f", 68 | target, 69 | source, 70 | work_directory=work_directory 71 | ) 72 | -------------------------------------------------------------------------------- /cleanroom/commands/pacman.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pacman command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.exceptions import ParseError 9 | from cleanroom.binarymanager import Binaries 10 | from cleanroom.command import Command 11 | from cleanroom.helper.archlinux.pacman import pacman 12 | from cleanroom.location import Location 13 | from cleanroom.systemcontext import SystemContext 14 | 15 | import typing 16 | 17 | 18 | class PacmanCommand(Command): 19 | """The pacman command.""" 20 | 21 | def __init__(self, **services: typing.Any) -> None: 22 | """Constructor.""" 23 | super().__init__( 24 | "pacman", 25 | target_distribution="arch", 26 | syntax=" [remove=False] " 27 | "[overwrite=GLOB] [assume_installed=PKG]", 28 | help_string="Run pacman to install .", 29 | file=__file__, 30 | **services 31 | ) 32 | 33 | def validate( 34 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 35 | ) -> None: 36 | """Validate the arguments.""" 37 | if not self._binary(Binaries.PACMAN) or not self._binary( 38 | Binaries.CHROOT_HELPER 39 | ): 40 | raise ParseError("No pacman binary was found.") 41 | 42 | self._validate_args_at_least( 43 | location, 44 | 1, 45 | '"{}"" needs at least ' "one package or group to install.", 46 | *args 47 | ) 48 | self._validate_kwargs( 49 | location, ("remove", "overwrite", "assume_installed",), **kwargs 50 | ) 51 | 52 | def __call__( 53 | self, 54 | location: Location, 55 | system_context: SystemContext, 56 | *args: typing.Any, 57 | **kwargs: typing.Any 58 | ) -> None: 59 | """Execute command.""" 60 | pacman( 61 | system_context, 62 | *args, 63 | remove=kwargs.get("remove", False), 64 | overwrite=kwargs.get("overwrite", ""), 65 | assume_installed=kwargs.get("assume_installed", ""), 66 | pacman_command=self._binary(Binaries.PACMAN), 67 | chroot_helper=self._binary(Binaries.CHROOT_HELPER) 68 | ) 69 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_intel_gpu.py: -------------------------------------------------------------------------------- 1 | """pkg_intel_gpu command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.location import Location 9 | from cleanroom.systemcontext import SystemContext 10 | 11 | import typing 12 | 13 | 14 | class PkgIntelGpuCommand(Command): 15 | """The pkg_intel_gpu command.""" 16 | 17 | def __init__(self, **services: typing.Any) -> None: 18 | """Constructor.""" 19 | super().__init__( 20 | "pkg_intel_gpu", help_string="Set up Intel GPU.", file=__file__, **services 21 | ) 22 | 23 | def validate( 24 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 25 | ) -> None: 26 | """Validate the arguments.""" 27 | self._validate_no_arguments(location, *args, **kwargs) 28 | 29 | def __call__( 30 | self, 31 | location: Location, 32 | system_context: SystemContext, 33 | *args: typing.Any, 34 | **kwargs: typing.Any 35 | ) -> None: 36 | """Execute command.""" 37 | 38 | # Enable KMS: 39 | self._execute(location, system_context, "pkg_intel_kms") 40 | 41 | self._execute(location, system_context, "pkg_xorg") 42 | 43 | # Set some kernel parameters: 44 | system_context.set_or_append_substitution( 45 | "KERNEL_CMDLINE", "intel_iommu=igfx_off i915.fastboot=1" 46 | ) 47 | 48 | self._execute( 49 | location, 50 | system_context, 51 | "pacman", 52 | "libva-intel-driver", 53 | "mesa", 54 | "vulkan-intel", 55 | "xf86-video-intel", 56 | "intel-media-driver", 57 | ) 58 | 59 | self._execute( 60 | location.next_line(), 61 | system_context, 62 | "create", 63 | "/etc/modprobe.d/i915-guc.conf", 64 | "options i915 enable_guc=3", 65 | ) 66 | 67 | self._execute( 68 | location.next_line(), 69 | system_context, 70 | "remove", 71 | "/usr/lib/firmware/amdgpu/*", 72 | "/usr/lib/firmware/nvidia/*", 73 | "/usr/lib/firmware/radeon/*", 74 | force=True, 75 | recursive=True, 76 | ) 77 | -------------------------------------------------------------------------------- /examples/system-example-desktop.def: -------------------------------------------------------------------------------- 1 | # Example system 2 | 3 | based_on type-desktop 4 | 5 | sed '/CHASSIS/ cCHASSIS="laptop"' /etc/machine.info 6 | set_hostname example pretty=Example 7 | set_machine_id bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 8 | 9 | pkg_intel_cpu 10 | pkg_intel_gpu 11 | 12 | pacman debootstrap modemmanager modem-manager-gui arch-install-scripts 13 | 14 | ## Mount /var in the sysroot: 15 | ## Adapt the mount unit to point to wherever you want your /var 16 | ## to be stored. 17 | ## Remove if you do not want /var to be persistent across reboots! 18 | create /usr/lib/systemd/system/sysroot-var.mount <<<<[Unit] 19 | Description=/var Directory (in sysroot) 20 | Documentation=man:hier(7) 21 | Before=initrd-fs.target initrd-parse-etc.service shutdown.target 22 | After=initrd-root-fs.target 23 | 24 | [Mount] 25 | What=/dev/disk/by-label/fs_btrfs 26 | Where=/sysroot/var 27 | Type=btrfs 28 | Options=compress=zstd,subvol=@var,nodev 29 | >>>> mode=0o644 30 | 31 | ## Optionally create a place to store image files. 32 | ## 33 | ## Clrm can loop-mount the root and verity partition straight from a image 34 | ## file in its initrd. This is rather convenient as you do not need to write 35 | ## the image file or the partitions contained therein to your drives, which 36 | ## might require you to create new partitions or override existing ones. 37 | ## 38 | ## This is used via the IMAGE_DEVICE, IMAGE_OPTIONS and IMAGE_FS variables. 39 | ## For this to work you you will need to tell clrm where the image files can 40 | ## be found. For the line below the following commands are needed: 41 | ## set IMAGE_DEVICE /dev/disk/by-label/fs_btrfs 42 | ## set IMAGE_OPTIONS compress=zstd,subvol=.images 43 | ## set IMAGE_FS btrfs 44 | ## 45 | ## The create_image command will take those options and inject code into 46 | ## the initrd to mount the image filesystem, to loop-mount the right image 47 | ## file from there (name must be "DISTRO_ID-DISTRO_VERSION_ID"). 48 | append /etc/fstab <<<>>> 51 | 52 | create /etc/modules-load.d/bluetooth.conf <<<>>> mode=0o644 54 | 55 | # Export a image: 56 | ## This will run create_image and export the result into the provided borg repository: 57 | export borg_repository 58 | 59 | -------------------------------------------------------------------------------- /tests/test_location.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Test for the location class. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import pytest # type: ignore 9 | 10 | import os 11 | import sys 12 | 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 14 | 15 | import cleanroom.location as loc 16 | 17 | 18 | @pytest.mark.parametrize( 19 | ("file_name", "line_number", "description", "result_string"), 20 | [ 21 | pytest.param(None, None, None, "", id="nothing"), 22 | pytest.param( 23 | "/tmp/foo", None, "!extra!", '/tmp/foo "!extra!"', id="file_name, extra" 24 | ), 25 | pytest.param("/tmp/foo", None, None, "/tmp/foo", id="file_name"), 26 | pytest.param("/tmp/foo", 1, None, "/tmp/foo:1", id="file_name, line_number=1"), 27 | pytest.param( 28 | "/tmp/foo", 42, None, "/tmp/foo:42", id="file_name, line_number=42" 29 | ), 30 | pytest.param( 31 | "/tmp/foo", 32 | 1, 33 | "!extra!", 34 | '/tmp/foo:1 "!extra!"', 35 | id="file_name, line_number=1, extra", 36 | ), 37 | pytest.param( 38 | "/tmp/foo", 39 | 42, 40 | "!extra!", 41 | '/tmp/foo:42 "!extra!"', 42 | id="file_name, line_number=42, extra", 43 | ), 44 | pytest.param(None, None, "!extra!", '"!extra!"', id="extra_info"), 45 | ], 46 | ) 47 | def test_location(file_name, line_number, description, result_string): 48 | location = loc.Location( 49 | file_name=file_name, line_number=line_number, description=description 50 | ) 51 | assert str(location) == result_string 52 | 53 | 54 | @pytest.mark.parametrize( 55 | ("file_name", "line_number", "description"), 56 | [ 57 | pytest.param(None, 42, None, id="line_number"), 58 | pytest.param("/tmp/foo", 0, None, id="file_name, invalid line_number"), 59 | pytest.param("/tmp/foo", -1, None, id="file_name, invalid line_number 2"), 60 | pytest.param(None, 42, "!extra!", id="line_number, extra"), 61 | ], 62 | ) 63 | def test_location_errors(file_name, line_number, description): 64 | with pytest.raises(AssertionError): 65 | loc.Location( 66 | file_name=file_name, line_number=line_number, description=description 67 | ) 68 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_intel_cpu.py: -------------------------------------------------------------------------------- 1 | """pkg_intel_cpu command. 2 | 3 | @author: Tobias Hunger 4 | """ 5 | 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.location import Location 9 | from cleanroom.systemcontext import SystemContext 10 | 11 | import os 12 | import typing 13 | 14 | 15 | class PkgIntelCpuCommand(Command): 16 | """The pkg_intel_cpu command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "pkg_intel_cpu", 22 | help_string="Install everything for intel CPU.", 23 | file=__file__, 24 | **services, 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any, 39 | ) -> None: 40 | """Execute command.""" 41 | 42 | # Nested virtualization: 43 | self._execute( 44 | location, 45 | system_context, 46 | "create", 47 | "/etc/modprobe.d/kvm_intel.conf", 48 | "options kvm_intel nested=1", 49 | ) 50 | 51 | # Intel ucode: 52 | location.set_description("Install intel-ucode") 53 | self._execute(location, system_context, "pacman", "intel-ucode") 54 | 55 | initrd_parts = os.path.join(system_context.boot_directory, "initrd-parts") 56 | os.makedirs(initrd_parts, exist_ok=True) 57 | self._execute( 58 | location, 59 | system_context, 60 | "move", 61 | "/boot/intel-ucode.img", 62 | os.path.join(initrd_parts, "00-intel-ucode"), 63 | to_outside=True, 64 | ) 65 | 66 | system_context.set_or_append_substitution( 67 | "INITRD_EXTRA_MODULES", "crc32c-intel" 68 | ) 69 | 70 | # Clean out firmware: 71 | self._execute( 72 | location.next_line(), 73 | system_context, 74 | "remove", 75 | "/usr/lib/firmware/amd-ucode/*", 76 | force=True, 77 | ) 78 | -------------------------------------------------------------------------------- /cleanroom/commands/set_hostname.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """set_hostname command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class SetHostnameCommand(Command): 17 | """The set_hostname command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "set_hostname", 23 | syntax=" [pretty=]", 24 | help_string="Set the hostname of the system.", 25 | file=__file__, 26 | **services, 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_args_exact(location, 1, '"{}" needs a static hostname.', *args) 34 | self._validate_kwargs(location, ("pretty",), **kwargs) 35 | 36 | def register_substitutions(self) -> typing.List[typing.Tuple[str, str, str]]: 37 | return [ 38 | ("HOSTNAME", "", "The hostname to be set"), 39 | ("PRETTY_HOSTNAME", "", "The pretty hostname to set"), 40 | ] 41 | 42 | def __call__( 43 | self, 44 | location: Location, 45 | system_context: SystemContext, 46 | *args: typing.Any, 47 | **kwargs: typing.Any, 48 | ) -> None: 49 | """Execute command.""" 50 | static_hostname = args[0] 51 | pretty_hostname = kwargs.get("pretty", static_hostname) 52 | 53 | if system_context.substitution("HOSTNAME", ""): 54 | raise GenerateError("Hostname was already set.", location=location) 55 | 56 | system_context.set_substitution("HOSTNAME", static_hostname) 57 | system_context.set_substitution("PRETTY_HOSTNAME", pretty_hostname) 58 | 59 | self._execute( 60 | location, system_context, "create", "/etc/hostname", static_hostname 61 | ) 62 | self._execute( 63 | location.next_line(), 64 | system_context, 65 | "sed", 66 | f'/^PRETTY_HOSTNAME=/ cPRETTY_HOSTNAME="{pretty_hostname}"', 67 | "/etc/machine.info", 68 | ) 69 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_avahi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pkg_avahi command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class PkgAvahiCommand(Command): 16 | """The pkg_avahi command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "pkg_avahi", 22 | help_string="Setup MDNS using avahi.", 23 | file=__file__, 24 | **services 25 | ) 26 | 27 | def validate( 28 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 29 | ) -> None: 30 | """Validate the arguments.""" 31 | self._validate_no_arguments(location, *args, **kwargs) 32 | 33 | def __call__( 34 | self, 35 | location: Location, 36 | system_context: SystemContext, 37 | *args: typing.Any, 38 | **kwargs: typing.Any 39 | ) -> None: 40 | """Execute command.""" 41 | self._execute(location, system_context, "pacman", "avahi") 42 | 43 | # Do setup: 44 | # Fix missing symlink: 45 | self._execute( 46 | location.next_line(), 47 | system_context, 48 | "symlink", 49 | "avahi-daemon.service", 50 | "dbus-org.freedesktop.Avahi.service", 51 | work_directory="/usr/lib/systemd/system", 52 | ) 53 | 54 | # enable the daemon (actually set up socket activation) 55 | self._execute( 56 | location.next_line(), 57 | system_context, 58 | "systemd_enable", 59 | "avahi-daemon.service", 60 | ) 61 | 62 | # Open the firewall for it: 63 | self._execute( 64 | location.next_line(), 65 | system_context, 66 | "net_firewall_open_port", 67 | "5353", 68 | protocol="udp", 69 | comment="Avahi", 70 | ) 71 | 72 | # Edit /etc/nsswitch.conf: 73 | self._execute( 74 | location.next_line(), 75 | system_context, 76 | "sed", 77 | "/^hosts\\s*:/ s/resolve/mdns_minimal " "[NOTFOUND=return] resolve/", 78 | "/etc/nsswitch.conf", 79 | ) 80 | -------------------------------------------------------------------------------- /cleanroom/commands/debootstrap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """debootstrap command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.exceptions import ParseError 11 | from cleanroom.helper.debian.apt import debootstrap 12 | from cleanroom.location import Location 13 | from cleanroom.systemcontext import SystemContext 14 | 15 | import typing 16 | 17 | 18 | class DebootstrapCommand(Command): 19 | """The debootstrap command.""" 20 | 21 | def __init__(self, **services: typing.Any) -> None: 22 | """Constructor.""" 23 | super().__init__( 24 | "debootstrap", 25 | target_distribution="debian", 26 | syntax="suite= " 27 | "mirror= [variant=] " 28 | "[include=] [exclude=]", 29 | help_string="Run debootstrap to install a in " 30 | "from . Include and exclude " 31 | "packages.", 32 | file=__file__, 33 | **services 34 | ) 35 | 36 | def validate( 37 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 38 | ) -> None: 39 | """Validate the arguments.""" 40 | self._validate_no_args(location, *args) 41 | self._validate_kwargs( 42 | location, ("suite", "mirror", "variant", "include", "exclude",), **kwargs 43 | ) 44 | self._require_kwargs(location, ("suite", "mirror",), **kwargs) 45 | 46 | def __call__( 47 | self, 48 | location: Location, 49 | system_context: SystemContext, 50 | *args: str, 51 | **kwargs: typing.Any 52 | ) -> None: 53 | """Execute command.""" 54 | debootstrap( 55 | system_context, 56 | suite=kwargs.get("suite", ""), 57 | target=system_context.fs_directory, 58 | mirror=kwargs.get("mirror", ""), 59 | variant=kwargs.get("variant", ""), 60 | include=kwargs.get("include", ""), 61 | exclude=kwargs.get("exclude", ""), 62 | debootstrap_command=self._binary(Binaries.DEBOOTSTRAP), 63 | ) 64 | 65 | location.set_description("Move systemd files into /usr") 66 | self._add_hook(location, system_context, "_teardown", "systemd_cleanup") 67 | -------------------------------------------------------------------------------- /cleanroom/firestarter/tarballinstalltarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Container-as-basic-filesystem installation target 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | import cleanroom.firestarter.tools as tool 11 | 12 | import os 13 | import typing 14 | 15 | 16 | def _tar(efi_fs: str, rootfs: str, *, tarball_name: str, efi_tarball_name: str) -> int: 17 | 18 | # Extract data 19 | result = 0 20 | if efi_tarball_name: 21 | result = tool.run( 22 | "/usr/bin/bash", 23 | "-c", 24 | f'( cd {efi_fs} ; tar -cf "{efi_tarball_name}" --auto-compress .) ', 25 | ).returncode 26 | if tarball_name: 27 | result += tool.run( 28 | "/usr/bin/bash", 29 | "-c", 30 | f'( cd {rootfs} ; tar -cf "{tarball_name}" --auto-compress .) ', 31 | ).returncode 32 | 33 | return result 34 | 35 | 36 | class TarballInstallTarget(InstallTarget): 37 | def __init__(self) -> None: 38 | super().__init__("tarball", "Creates a tarball from the system image.") 39 | 40 | def setup_subparser(self, subparser: typing.Any) -> None: 41 | subparser.add_argument( 42 | "--efi-tarball", 43 | action="store", 44 | dest="efi_tarball", 45 | help="The tarball containing the EFI partition. [Default: empty -- skip]", 46 | ) 47 | 48 | subparser.add_argument( 49 | "--tarball", 50 | action="store", 51 | dest="tarball", 52 | help="The tarball containing the root filesystem image [Default: empty -- skip].", 53 | ) 54 | 55 | def __call__( 56 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 57 | ) -> int: 58 | if not parse_result.tarball and not parse_result.efi_tarball: 59 | return 1 60 | 61 | assert os.path.isfile(image_file) 62 | 63 | # Mount filessystems and copy the rootfs into import_dir: 64 | return tool.execute_with_system_mounted( 65 | lambda e, r: _tar( 66 | e, 67 | r, 68 | tarball_name=parse_result.tarball, 69 | efi_tarball_name=parse_result.efi_tarball, 70 | ), 71 | image_file=image_file, 72 | tmp_dir=tmp_dir, 73 | ) 74 | -------------------------------------------------------------------------------- /cleanroom/executor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The class that runs a list of commands on a system. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from .commandmanager import CommandManager 9 | from .execobject import ExecObject 10 | from .printer import success 11 | from .systemcontext import SystemContext 12 | 13 | import os 14 | import typing 15 | 16 | 17 | class Executor: 18 | """Run a list of ExecObjects on a system.""" 19 | 20 | def __init__( 21 | self, 22 | *, 23 | scratch_directory: str, 24 | systems_definition_directory: str, 25 | command_manager: CommandManager, 26 | repository_base_directory: str, 27 | timestamp: str, 28 | ) -> None: 29 | assert scratch_directory 30 | assert systems_definition_directory 31 | 32 | self._scratch_directory = scratch_directory 33 | self._systems_definition_directory = systems_definition_directory 34 | self._command_manager = command_manager 35 | self._timestamp = timestamp 36 | self._repository_base_directory = repository_base_directory 37 | 38 | def run( 39 | self, 40 | system_name: str, 41 | base_system_name: typing.Optional[str], 42 | exec_obj_list: typing.List[ExecObject], 43 | storage_directory: str, 44 | ) -> None: 45 | """Run the command_list for the system the executor was set up for.""" 46 | with SystemContext( 47 | system_name=system_name, 48 | base_system_name=base_system_name or "", 49 | scratch_directory=self._scratch_directory, 50 | systems_definition_directory=self._systems_definition_directory, 51 | storage_directory=storage_directory, 52 | repository_base_directory=self._repository_base_directory, 53 | timestamp=self._timestamp, 54 | ) as system_context: 55 | self._command_manager.setup_substitutions(system_context) 56 | 57 | for exec_obj in exec_obj_list: 58 | os.chdir(system_context.systems_definition_directory) 59 | command = self._command_manager.command(exec_obj.command) 60 | assert command 61 | command.execute_func( 62 | exec_obj.location, system_context, exec_obj.args, exec_obj.kwargs 63 | ) 64 | success(f"System {system_name} created successfully.") 65 | -------------------------------------------------------------------------------- /cleanroom/commands/_create_dmverity_fsimage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_create_dmverity_fsimage command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.location import Location 11 | from cleanroom.helper.file import size_extend 12 | from cleanroom.helper.run import run 13 | from cleanroom.systemcontext import SystemContext 14 | 15 | 16 | import typing 17 | 18 | 19 | class CreateDmverityFsimageCommand(Command): 20 | """The _create_dmverity_fsimage Command.""" 21 | 22 | def __init__(self, **services: typing.Any) -> None: 23 | """Constructor.""" 24 | super().__init__( 25 | "_create_dmverity_fsimage", 26 | syntax="DMVERITY_IMAGE FILE " "[base_image= None: 35 | """Validate arguments.""" 36 | self._validate_args_exact( 37 | location, 1, "{} needs a filename for the dm-verity image.", *args 38 | ) 39 | self._validate_kwargs(location, ("base_image",), **kwargs) 40 | 41 | def __call__( 42 | self, 43 | location: Location, 44 | system_context: SystemContext, 45 | *args: typing.Any, 46 | **kwargs: typing.Any 47 | ) -> None: 48 | """Execute command.""" 49 | verity_file = args[0] 50 | base_image = kwargs.get("base_image", "") 51 | assert base_image 52 | 53 | result = run( 54 | self._binary(Binaries.VERITYSETUP), "format", base_image, verity_file 55 | ) 56 | 57 | size_extend(verity_file) 58 | 59 | root_hash: typing.Optional[str] = None 60 | uuid: typing.Optional[str] = None 61 | for line in result.stdout.split("\n"): 62 | if line.startswith("Root hash:"): 63 | root_hash = line[10:].strip() 64 | if line.startswith("UUID:"): 65 | uuid = line[10:].strip() 66 | 67 | assert root_hash is not None 68 | assert uuid is not None 69 | 70 | system_context.set_substitution("LAST_DMVERITY_UUID", uuid) 71 | system_context.set_substitution("LAST_DMVERITY_ROOTHASH", root_hash) 72 | -------------------------------------------------------------------------------- /cleanroom/commands/_restore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_restore command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import os 13 | import typing 14 | 15 | 16 | class RestoreCommand(Command): 17 | """The _restore command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "_restore", 23 | syntax=" [pretty=]", 24 | help_string="Set the hostname of the system.", 25 | file=__file__, 26 | **services 27 | ) 28 | 29 | def validate( 30 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 31 | ) -> None: 32 | """Validate the arguments.""" 33 | self._validate_arguments_exact( 34 | location, 1, '"{}" needs a base system to restore.', *args, **kwargs 35 | ) 36 | 37 | def __call__( 38 | self, 39 | location: Location, 40 | system_context: SystemContext, 41 | *args: typing.Any, 42 | **kwargs: typing.Any 43 | ) -> None: 44 | """Execute command.""" 45 | 46 | base = args[0] 47 | assert ( 48 | system_context.base_context 49 | and system_context.base_context.system_name == base 50 | ) 51 | 52 | btrfs_helper = self._service("btrfs_helper") 53 | 54 | if not os.path.isdir(system_context.scratch_directory): 55 | btrfs_helper.create_subvolume(system_context.scratch_directory) 56 | btrfs_helper.set_property( 57 | system_context.scratch_directory, name="compression", value="none" 58 | ) 59 | 60 | btrfs_helper.create_snapshot( 61 | os.path.join(system_context.base_storage_directory, "meta"), 62 | system_context.meta_directory, 63 | ) 64 | btrfs_helper.create_snapshot( 65 | os.path.join(system_context.base_storage_directory, "boot"), 66 | system_context.boot_directory, 67 | ) 68 | btrfs_helper.create_snapshot( 69 | os.path.join(system_context.base_storage_directory, "fs"), 70 | system_context.fs_directory, 71 | ) 72 | 73 | btrfs_helper.create_subvolume(system_context.cache_directory) 74 | -------------------------------------------------------------------------------- /cleanroom/commands/usermod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """usermod command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import typing 14 | 15 | 16 | class UsermodCommand(Command): 17 | """The usermod command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "usermod", 23 | syntax=" [comment=] [home=] " 24 | "[gid=] [uid=] [rename=] " 25 | "[groups=,] [lock=False] " 26 | "[password=] [shell=] " 27 | "[expire=], [append=False]", 28 | help_string="Modify an existing user.", 29 | file=__file__, 30 | **services 31 | ) 32 | 33 | def validate( 34 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 35 | ) -> None: 36 | """Validate the arguments.""" 37 | self._validate_args_exact(location, 1, '"{}" needs a username.', *args) 38 | if len(kwargs) == 0: 39 | raise ParseError("usermod needs keyword arguments", location=location) 40 | 41 | lock = kwargs.get("lock", None) 42 | if lock not in (True, None, False): 43 | raise ParseError( 44 | '"lock" must be either True, False or None.', location=location 45 | ) 46 | 47 | append = kwargs.get("append", False) 48 | if append not in (True, False): 49 | raise ParseError( 50 | '"append" must have either True or False as ' "value.", 51 | location=location, 52 | ) 53 | 54 | if append and kwargs.get("groups", "") == "": 55 | raise ParseError( 56 | '"append" needs "groups" to be set, too.', location=location 57 | ) 58 | 59 | def __call__( 60 | self, 61 | location: Location, 62 | system_context: SystemContext, 63 | *args: typing.Any, 64 | **kwargs: typing.Any 65 | ) -> None: 66 | """Execute command.""" 67 | self._service("user_helper").usermod( 68 | args[0], **kwargs, root_directory=system_context.fs_directory 69 | ) 70 | -------------------------------------------------------------------------------- /cleanroom/commands/set_machine_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """set_machine_id command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError, ParseError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import re 14 | import typing 15 | 16 | 17 | class SetMachineIdCommand(Command): 18 | """The set_machine_id command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "set_machine_id", 24 | syntax="", 25 | help_string="Set the machine id of the system.", 26 | file=__file__, 27 | **services, 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_arguments_exact( 35 | location, 1, '"{}" needs the machine id.', *args, **kwargs 36 | ) 37 | 38 | machine_id = args[0] 39 | assert machine_id 40 | 41 | id_pattern = re.compile("^[A-Fa-f0-9]{32}$") 42 | if not id_pattern.match(machine_id): 43 | raise ParseError( 44 | f'"{machine_id}" is not a valid machine-id.', location=location 45 | ) 46 | 47 | def register_substitutions(self) -> typing.List[typing.Tuple[str, str, str]]: 48 | return [ 49 | ( 50 | "MACHINE_ID", 51 | "", 52 | "The machine id for the system. Only valid after set_machine_id was called", 53 | ), 54 | ] 55 | 56 | def __call__( 57 | self, 58 | location: Location, 59 | system_context: SystemContext, 60 | *args: typing.Any, 61 | **kwargs: typing.Any, 62 | ) -> None: 63 | """Execute command.""" 64 | old_machine_id = system_context.substitution("MACHINE_ID", "") 65 | if old_machine_id: 66 | raise GenerateError( 67 | f'Machine-id was already set to "{old_machine_id}".', location=location, 68 | ) 69 | 70 | machine_id = args[0] 71 | system_context.set_substitution("MACHINE_ID", machine_id) 72 | machine_id += "\n" 73 | self._execute( 74 | location.next_line(), 75 | system_context, 76 | "create", 77 | "/etc/machine-id", 78 | machine_id, 79 | ) 80 | -------------------------------------------------------------------------------- /cleanroom/commands/systemd_set_default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """systemd_set_default command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError 10 | from cleanroom.helper.file import isfile 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | 16 | 17 | class SystemdSetDefaultCommand(Command): 18 | """The systemd_set_default command.""" 19 | 20 | def __init__(self, **services: typing.Any) -> None: 21 | """Constructor.""" 22 | super().__init__( 23 | "systemd_set_default", 24 | syntax="", 25 | help_string="Set the systemd target to boot into.", 26 | file=__file__, 27 | **services, 28 | ) 29 | 30 | def validate( 31 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 32 | ) -> None: 33 | """Validate the arguments.""" 34 | self._validate_arguments_exact( 35 | location, 1, '"{}" needs a target name.', *args, **kwargs 36 | ) 37 | 38 | def register_substitutions(self) -> typing.List[typing.Tuple[str, str, str]]: 39 | return [ 40 | ( 41 | "DEFAULT_BOOT_TARGET", 42 | "multi-user.target", 43 | "The systemd target to boot into", 44 | ), 45 | ] 46 | 47 | def __call__( 48 | self, 49 | location: Location, 50 | system_context: SystemContext, 51 | *args: typing.Any, 52 | **kwargs: typing.Any, 53 | ) -> None: 54 | """Execute command.""" 55 | target = args[0] 56 | systemd_directory = "/usr/lib/systemd/system/" 57 | target_path = systemd_directory + args[0] 58 | 59 | if not isfile(system_context, target_path): 60 | raise GenerateError( 61 | f'Target "{target}" does not exist or is no file. Can not use as default target.' 62 | ) 63 | 64 | default = "default.target" 65 | default_path = systemd_directory + "default.target" 66 | 67 | self._execute(location, system_context, "remove", default_path, force=True) 68 | self._execute( 69 | location.next_line(), 70 | system_context, 71 | "symlink", 72 | target, 73 | default, 74 | work_directory=systemd_directory, 75 | ) 76 | 77 | system_context.set_substitution("DEFAULT_BOOT_TARGET", target) 78 | -------------------------------------------------------------------------------- /cleanroom/location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The class that holds a location in source. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from __future__ import annotations 9 | 10 | import typing 11 | 12 | 13 | class Location: 14 | """Context data for the execution os commands.""" 15 | 16 | def __init__( 17 | self, 18 | *, 19 | file_name: typing.Optional[str] = None, 20 | line_number: typing.Optional[int] = None, 21 | description: typing.Optional[str] = None, 22 | parent: typing.Optional[Location] = None, 23 | ) -> None: 24 | """Constructor.""" 25 | if line_number is not None: 26 | assert line_number > 0 27 | assert file_name is not None 28 | 29 | self.file_name = file_name 30 | self.line_number = line_number 31 | self.description = description 32 | self.parent = parent 33 | 34 | def is_valid(self) -> bool: 35 | """Check whether this object contains a valid location.""" 36 | return self.file_name is not None or self.description is not None 37 | 38 | def set_description(self, message: str) -> None: 39 | """Set location description.""" 40 | self.description = message 41 | 42 | def create_child( 43 | self, 44 | *, 45 | file_name: typing.Optional[str] = None, 46 | line_number: typing.Optional[int] = None, 47 | description: typing.Optional[str] = None, 48 | ) -> "Location": 49 | return Location( 50 | file_name=file_name, 51 | line_number=line_number, 52 | description=description, 53 | parent=self, 54 | ) 55 | 56 | def next_line(self) -> "Location": 57 | if self.line_number is None: 58 | self.line_number = 1 59 | else: 60 | self.line_number += 1 61 | return self 62 | 63 | def __str__(self) -> str: 64 | """Stringify location.""" 65 | if self.file_name is None: 66 | if self.description: 67 | result = f'"{self.description}"' 68 | else: 69 | result = "" 70 | else: 71 | result = self.file_name 72 | 73 | if self.line_number is not None and self.line_number > 0: 74 | result = f"{result}:{self.line_number}" 75 | 76 | if self.description is not None: 77 | result = f'{result} "{self.description}"' 78 | 79 | if self.parent is not None: 80 | result = f"{str(self.parent)} => {result}" 81 | return result 82 | -------------------------------------------------------------------------------- /cleanroom/commands/install_certificate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """install_certificate command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.run import run 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import os 15 | import os.path 16 | import stat 17 | import typing 18 | 19 | 20 | class InstallCertificatesCommand(Command): 21 | """The install_certificate command.""" 22 | 23 | def __init__(self, **services: typing.Any) -> None: 24 | """Constructor.""" 25 | super().__init__( 26 | "install_certificate", 27 | syntax="+", 28 | help_string="Install CA certificates.", 29 | file=__file__, 30 | **services 31 | ) 32 | 33 | def validate( 34 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 35 | ) -> None: 36 | """Validate the arguments.""" 37 | self._validate_arguments_at_least( 38 | location, 39 | 1, 40 | '"{}" needs at least one ' "ca certificate to add", 41 | *args, 42 | **kwargs 43 | ) 44 | 45 | def __call__( 46 | self, 47 | location: Location, 48 | system_context: SystemContext, 49 | *args: typing.Any, 50 | **kwargs: typing.Any 51 | ) -> None: 52 | """Execute command.""" 53 | for f in args: 54 | source = ( 55 | f 56 | if os.path.isabs(f) 57 | else os.path.join(system_context.systems_definition_directory or "", f) 58 | ) 59 | dest = os.path.join( 60 | "/etc/ca-certificates/trust-source/anchors", os.path.basename(f) 61 | ) 62 | self._execute( 63 | location.next_line(), 64 | system_context, 65 | "copy", 66 | source, 67 | dest, 68 | from_outside=True, 69 | ) 70 | self._execute( 71 | location.next_line(), 72 | system_context, 73 | "chmod", 74 | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH, 75 | dest, 76 | ) 77 | 78 | run( 79 | "/usr/bin/trust", 80 | "extract-compat", 81 | chroot=system_context.fs_directory, 82 | chroot_helper=self._binary(Binaries.CHROOT_HELPER), 83 | ) 84 | -------------------------------------------------------------------------------- /cleanroom/commands/based_on.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """based_on command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import ParseError 10 | from cleanroom.location import Location 11 | from cleanroom.systemcontext import SystemContext 12 | from cleanroom.printer import verbose 13 | 14 | import re 15 | import typing 16 | 17 | 18 | class BasedOnCommand(Command): 19 | """The based_on command.""" 20 | 21 | def __init__(self, **services: typing.Any) -> None: 22 | """Constructor.""" 23 | super().__init__( 24 | "based_on", 25 | syntax=")", 26 | help_string="Use as a base for this " 27 | 'system. Use "scratch" to start from a ' 28 | "blank slate.\n\n" 29 | "Note: This command needs to be the first in the " 30 | "system definition file!", 31 | file=__file__, 32 | **services, 33 | ) 34 | 35 | def validate( 36 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 37 | ) -> None: 38 | """Validate the arguments.""" 39 | self._validate_arguments_exact( 40 | location, 1, '"{}" needs a system name.', *args, **kwargs 41 | ) 42 | base = args[0] 43 | 44 | system_pattern = re.compile("^[A-Za-z][A-Za-z0-9_-]*$") 45 | if not system_pattern.match(base): 46 | raise ParseError( 47 | f'"{self.name}" got invalid base system name "{base}".', 48 | location=location, 49 | ) 50 | 51 | def dependency(self, *args: typing.Any, **kwargs: typing.Any) -> str: 52 | return args[0] 53 | 54 | def __call__( 55 | self, 56 | location: Location, 57 | system_context: SystemContext, 58 | *args: typing.Any, 59 | **kwargs: typing.Any, 60 | ) -> None: 61 | """Execute command.""" 62 | 63 | base_system = args[0] 64 | 65 | if base_system == "scratch": 66 | assert system_context.base_context is None 67 | verbose("Building from scratch!") 68 | self._add_hook(location, system_context, "testing", "_test") 69 | self._execute(location, system_context, "_setup") 70 | else: 71 | assert ( 72 | system_context.base_context 73 | and system_context.base_context.system_name == base_system 74 | ) 75 | verbose(f"Building on top of {base_system}.") 76 | self._execute(location, system_context, "_restore", base_system) 77 | 78 | self._run_hooks(system_context, "_setup") 79 | -------------------------------------------------------------------------------- /cleanroom/commands/_create_root_fsimage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_create_root_fsimage command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.exceptions import GenerateError 11 | from cleanroom.location import Location 12 | from cleanroom.helper.file import size_extend 13 | from cleanroom.helper.run import run 14 | from cleanroom.systemcontext import SystemContext 15 | 16 | import typing 17 | 18 | 19 | class CreateRootFsimageCommand(Command): 20 | """The _create_root_fsimage Command.""" 21 | 22 | def __init__(self, **services: typing.Any) -> None: 23 | """Constructor.""" 24 | self._root_partition = "" 25 | self._verity_partition = "" 26 | 27 | self._root_hash = "" 28 | 29 | self._skip_validation = False 30 | 31 | super().__init__( 32 | "_create_root_fsimage", 33 | syntax=" [usr_only=True]", 34 | help_string="Create a root filesystem image", 35 | file=__file__, 36 | **services 37 | ) 38 | 39 | def validate( 40 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 41 | ) -> None: 42 | """Validate arguments.""" 43 | self._validate_args_exact( 44 | location, 1, "{} needs a file name for the root filesystem image.", *args 45 | ) 46 | self._validate_kwargs(location, ("usr_only",), **kwargs) 47 | 48 | def __call__( 49 | self, 50 | location: Location, 51 | system_context: SystemContext, 52 | *args: typing.Any, 53 | **kwargs: typing.Any 54 | ) -> None: 55 | """Execute command.""" 56 | self._usr_only = kwargs.get("usr_only", True) 57 | 58 | rootfs_file = args[0] 59 | 60 | rootfs_label = system_context.substitution_expanded("ROOTFS_PARTLABEL", "") 61 | if not rootfs_label: 62 | raise GenerateError("ROOTFS_PARTLABEL is unset.") 63 | target_directory = "usr" if self._usr_only else "." 64 | target_args = ["-keep-as-directory"] if self._usr_only else [] 65 | run( 66 | self._binary(Binaries.MKSQUASHFS), 67 | target_directory, 68 | rootfs_file, 69 | *target_args, 70 | "-comp", 71 | "gzip", # compression does not matter: We disable compression! 72 | "-noappend", 73 | "-no-exports", 74 | "-noI", 75 | "-noD", 76 | "-noF", 77 | "-noX", 78 | "-processors", 79 | "1", 80 | work_directory=system_context.fs_directory 81 | ) 82 | size_extend(rootfs_file) 83 | -------------------------------------------------------------------------------- /cleanroom/commands/_export_directory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_export_directory command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.run import run 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import typing 15 | import os 16 | 17 | 18 | class ExportDirectoryCommand(Command): 19 | """The _export_directory command.""" 20 | 21 | def __init__(self, **services: typing.Any) -> None: 22 | """Constructor.""" 23 | super().__init__( 24 | "_export_directory", 25 | syntax=" " 26 | "compression= " 27 | "compression_level=<5> " 28 | "repository=", 29 | help_string="Export a directory from cleanroom.", 30 | file=__file__, 31 | **services, 32 | ) 33 | 34 | def validate( 35 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 36 | ) -> None: 37 | """Validate the arguments.""" 38 | self._validate_args_exact( 39 | location, 40 | 1, 41 | '"{}" needs a borg repository directory ' "to export into.", 42 | *args, 43 | ) 44 | self._validate_kwargs( 45 | location, ("compression", "compression_level", "repository"), **kwargs 46 | ) 47 | self._require_kwargs(location, ("repository",), **kwargs) 48 | 49 | def __call__( 50 | self, 51 | location: Location, 52 | system_context: SystemContext, 53 | *args: typing.Any, 54 | **kwargs: typing.Any, 55 | ) -> None: 56 | """Execute command.""" 57 | export_directory = args[0] 58 | export_repository = os.path.join( 59 | system_context.repository_base_directory, kwargs.get("repository", "") 60 | ) 61 | 62 | backup_name = system_context.system_name + "-" + system_context.timestamp 63 | 64 | env = os.environ 65 | env["BORG_UNKNOWN_UNENCRYPTED_ACCESS_IS_OK"] = "yes" 66 | env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = "yes" 67 | 68 | comp = kwargs.get("compression", "zstd") 69 | comp_level = kwargs.get("compression_level", 5) 70 | 71 | run( 72 | self._service("binary_manager").binary(Binaries.BORG), 73 | "create", 74 | "--compression", 75 | f"{comp},{comp_level}", 76 | "--numeric-owner", 77 | "--noatime", 78 | f"{export_repository}::{backup_name}", 79 | ".", 80 | work_directory=export_directory, 81 | env=env, 82 | ) 83 | -------------------------------------------------------------------------------- /cleanroom/commands/create_os_release.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """create_os_release command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import typing 13 | 14 | 15 | class CreateOsReleaseCommand(Command): 16 | """The create_os_release command.""" 17 | 18 | def __init__(self, **services: typing.Any) -> None: 19 | """Constructor.""" 20 | super().__init__( 21 | "create_os_release", 22 | syntax="", 23 | help_string="Create os release file.", 24 | file=__file__, 25 | **services 26 | ) 27 | 28 | def validate( 29 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 30 | ) -> None: 31 | """Validate arguments.""" 32 | self._validate_no_arguments(location, *args, **kwargs) 33 | 34 | def __call__( 35 | self, 36 | location: Location, 37 | system_context: SystemContext, 38 | *args: typing.Any, 39 | **kwargs: typing.Any 40 | ) -> None: 41 | """Execute command.""" 42 | os_release = 'NAME="{}"\n'.format( 43 | system_context.substitution_expanded("DISTRO_NAME", "") 44 | ) 45 | os_release += 'PRETTY_NAME="{}"\n'.format( 46 | system_context.substitution_expanded("DISTRO_PRETTY_NAME", "") 47 | ) 48 | os_release += 'ID="{}"\n'.format( 49 | system_context.substitution_expanded("DISTRO_ID", "") 50 | ) 51 | os_release += 'ID_LIKE="{}"\n'.format( 52 | system_context.substitution_expanded("DISTRO_ID_LIKE", "") 53 | ) 54 | os_release += 'ANSI_COLOR="{}"\n'.format( 55 | system_context.substitution_expanded("DISTRO_ANSI_COLOR", "") 56 | ) 57 | os_release += 'HOME_URL="{}"\n'.format( 58 | system_context.substitution_expanded("DISTRO_HOME_URL", "") 59 | ) 60 | os_release += 'SUPPORT_URL="{}"\n'.format( 61 | system_context.substitution_expanded("DISTRO_SUPPORT_URL", "") 62 | ) 63 | os_release += 'BUG_REPORT_URL="{}"\n'.format( 64 | system_context.substitution_expanded("DISTRO_BUG_URL", "") 65 | ) 66 | os_release += 'VERSION="{}"\n'.format( 67 | system_context.substitution_expanded("DISTRO_VERSION", "") 68 | ) 69 | os_release += 'VERSION_ID="{}"\n'.format( 70 | system_context.substitution_expanded("DISTRO_VERSION_ID", "") 71 | ) 72 | 73 | self._execute( 74 | location, 75 | system_context, 76 | "create", 77 | "/usr/lib/os-release", 78 | os_release, 79 | force=True, 80 | mode=0o644, 81 | ) 82 | -------------------------------------------------------------------------------- /cleanroom/commands/sshd_set_hostkeys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """sshd_set_hostkeys command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.exceptions import GenerateError, ParseError 10 | from cleanroom.helper.file import chmod, chown, isdir 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | 14 | import glob 15 | import os 16 | import typing 17 | 18 | 19 | def _key_files(key_directory: str) -> str: 20 | return os.path.join(key_directory, "ssh_host_*_key*") 21 | 22 | 23 | class SshdSetHostkeysCommand(Command): 24 | """The sshd_set_hostkeys command.""" 25 | 26 | def __init__(self, **services: typing.Any) -> None: 27 | """Constructor.""" 28 | super().__init__( 29 | "sshd_set_hostkeys", 30 | syntax=")", 31 | help_string="Install all the ssh_host_*_key files found in " 32 | " for SSHD.", 33 | file=__file__, 34 | **services, 35 | ) 36 | 37 | def validate( 38 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 39 | ) -> None: 40 | """Validate the arguments.""" 41 | self._validate_arguments_exact( 42 | location, 1, '"{}" needs a directory with ' "host keys.", *args 43 | ) 44 | 45 | def _validate_key_directory(self, location: Location, key_directory: str) -> None: 46 | if not os.path.isdir(key_directory): 47 | raise ParseError( 48 | f'"{self.name}": {key_directory} must be a directory (work directory is {os.getcwd()}).' 49 | ) 50 | 51 | keyfiles = glob.glob(_key_files(key_directory)) 52 | if not keyfiles: 53 | raise ParseError( 54 | f'"{self.name}": No ssh_host_*_key files found in {key_directory}.', 55 | location=location, 56 | ) 57 | 58 | def __call__( 59 | self, 60 | location: Location, 61 | system_context: SystemContext, 62 | *args: typing.Any, 63 | **kwargs: typing.Any, 64 | ) -> None: 65 | """Execute command.""" 66 | key_directory = args[0] 67 | self._validate_key_directory(location, key_directory) 68 | if not isdir(system_context, "/etc/ssh"): 69 | os.makedirs(system_context.file_name("/etc/ssh")) 70 | 71 | self._execute( 72 | location, 73 | system_context, 74 | "copy", 75 | _key_files(key_directory), 76 | "/etc/ssh", 77 | from_outside=True, 78 | ) 79 | chown(system_context, "root", "root", _key_files("/etc/ssh")) 80 | chmod(system_context, 0o600, "/etc/ssh/ssh_host_*_key") 81 | chmod(system_context, 0o644, "/etc/ssh/ssh_host_*_key.pub") 82 | -------------------------------------------------------------------------------- /cleanroom/helper/group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """group manipulation print_commands. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from .run import run 9 | 10 | import os 11 | import typing 12 | 13 | 14 | class Group(typing.NamedTuple): 15 | name: str 16 | password: str 17 | gid: int 18 | members: typing.List[str] 19 | 20 | 21 | def _group_data(group_file: str, name: str) -> typing.Optional[Group]: 22 | if not os.path.exists(group_file): 23 | return None 24 | 25 | with open(group_file, "r") as group: 26 | for line in group: 27 | if line.endswith("\n"): 28 | line = line[:-1] 29 | current_group: typing.Any = line.split(":") 30 | if current_group[0] == name: 31 | current_group[2] = int(current_group[2]) 32 | if current_group[3] == "": 33 | current_group[3] = [] 34 | else: 35 | current_group[3] = list(current_group[3].split(",")) 36 | return Group(*current_group) 37 | return Group("nobody", "x", 65534, []) 38 | 39 | 40 | class GroupHelper: 41 | def __init__(self, add_command: str, mod_command: str) -> None: 42 | self._add_command = add_command 43 | self._mod_command = mod_command 44 | 45 | def groupadd( 46 | self, 47 | group_name: str, 48 | *, 49 | gid: int = -1, 50 | force: bool = False, 51 | system: bool = False, 52 | root_directory: str 53 | ) -> bool: 54 | """Execute command.""" 55 | command_line = [self._add_command, "--root", root_directory, group_name] 56 | 57 | if gid >= 0: 58 | command_line += ["--gid", str(gid)] 59 | 60 | if force: 61 | command_line += ["--force"] 62 | 63 | if system: 64 | command_line += ["--system"] 65 | 66 | return run(*command_line).returncode == 0 67 | 68 | @staticmethod 69 | def group_data(name: str, *, root_directory: str) -> typing.Optional[Group]: 70 | """Get group data from group file.""" 71 | return _group_data(os.path.join(root_directory, "etc/group"), name) 72 | 73 | def groupmod( 74 | self, 75 | group_name: str, 76 | *, 77 | gid: int = -1, 78 | password: str = "", 79 | rename: str = "", 80 | root_directory: str = "" 81 | ) -> bool: 82 | """Modify an existing group.""" 83 | command_line = [self._mod_command, "--root", root_directory, group_name] 84 | 85 | if gid >= 0: 86 | command_line += ["--gid", str(gid)] 87 | 88 | if rename: 89 | command_line += ["--new-name", rename] 90 | 91 | if password: 92 | command_line += ["--password", password] 93 | 94 | return run(*command_line).returncode == 0 95 | -------------------------------------------------------------------------------- /cleanroom/commands/sign_efi_binary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """sign_efi_binary command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.binarymanager import Binaries 9 | from cleanroom.command import Command 10 | from cleanroom.helper.run import run 11 | from cleanroom.location import Location 12 | from cleanroom.systemcontext import SystemContext 13 | from cleanroom.printer import info, trace 14 | 15 | import os 16 | import typing 17 | 18 | 19 | class SignEfiBinaryCommand(Command): 20 | """The sign_efi_binary command.""" 21 | 22 | def __init__(self, **services: typing.Any) -> None: 23 | """Constructor.""" 24 | super().__init__( 25 | "sign_efi_binary", 26 | syntax=" [key=] [cert=] [outside=False] " 27 | "[keep_unsigned=False]", 28 | help_string="Sign using and .", 29 | file=__file__, 30 | **services, 31 | ) 32 | 33 | def validate( 34 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 35 | ) -> None: 36 | """Validate the arguments.""" 37 | self._validate_args_exact(location, 1, '"{}" needs a file to sign.', *args) 38 | self._validate_kwargs( 39 | location, ("key", "cert", "outside", "keep_unsigned"), **kwargs 40 | ) 41 | 42 | def __call__( 43 | self, 44 | location: Location, 45 | system_context: SystemContext, 46 | *args: typing.Any, 47 | **kwargs: typing.Any, 48 | ) -> None: 49 | """Execute command.""" 50 | to_sign = args[0] 51 | keep_unsigned = kwargs.get("keep_unsigned", False) 52 | if not kwargs.get("outside", False): 53 | to_sign = system_context.file_name(to_sign) 54 | systems_directory = system_context.systems_definition_directory 55 | key = os.path.join(systems_directory, kwargs.get("key", "config/efi/sign.key")) 56 | cert = os.path.join( 57 | systems_directory, kwargs.get("cert", "config/efi/sign.crt") 58 | ) 59 | 60 | info( 61 | f"Signing EFI binary {to_sign} using key {key} and cert {cert} (keep unsigned: {keep_unsigned})." 62 | ) 63 | 64 | signed = to_sign + ".signed" 65 | 66 | assert os.path.isfile(key) 67 | assert os.path.isfile(cert) 68 | assert os.path.isfile(to_sign) 69 | assert not os.path.exists(signed) 70 | 71 | run( 72 | self._binary(Binaries.SBSIGN), 73 | "--key", 74 | key, 75 | "--cert", 76 | cert, 77 | "--output", 78 | signed, 79 | to_sign, 80 | ) 81 | 82 | if keep_unsigned: 83 | assert os.path.isfile(to_sign) 84 | assert os.path.isfile(signed) 85 | else: 86 | trace(f"Moving {signed} to {to_sign}.") 87 | os.remove(to_sign) 88 | os.rename(signed, to_sign) 89 | 90 | assert os.path.isfile(to_sign) 91 | -------------------------------------------------------------------------------- /cleanroom/firestarter/containerfsinstalltarget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Container-as-basic-filesystem installation target 4 | 5 | @author: Tobias Hunger 6 | """ 7 | 8 | 9 | from cleanroom.firestarter.installtarget import InstallTarget 10 | import cleanroom.firestarter.tools as tool 11 | from cleanroom.helper.btrfs import BtrfsHelper 12 | 13 | import os 14 | import typing 15 | 16 | 17 | def _extract_into_snapshot(_, rootfs: str, *, import_snapshot: str) -> int: 18 | # Extract data 19 | return tool.run( 20 | "/usr/bin/bash", 21 | "-c", 22 | f'( cd "{rootfs}" ; tar -cf - . ) | ( cd "{import_snapshot}" ; tar -xf - )', 23 | ).returncode 24 | 25 | 26 | class ContainerFilesystemInstallTarget(InstallTarget): 27 | def __init__(self) -> None: 28 | super().__init__("container_fs", "Install a container filesystem.") 29 | 30 | def __call__( 31 | self, *, parse_result: typing.Any, tmp_dir: str, image_file: str 32 | ) -> int: 33 | container_name = parse_result.override_system_name 34 | if not container_name: 35 | container_name = parse_result.system_name 36 | if container_name.startswith("system-"): 37 | container_name = container_name[7:] 38 | read_write = parse_result.read_write 39 | 40 | container_dir = os.path.join(parse_result.machines_dir, container_name) 41 | import_dir = container_dir + "_import" 42 | 43 | try: 44 | btrfs = BtrfsHelper("/usr/bin/btrfs") 45 | btrfs.create_subvolume(import_dir) 46 | 47 | # Mount filessystems and copy the rootfs into import_dir: 48 | result = tool.execute_with_system_mounted( 49 | lambda e, r: _extract_into_snapshot(e, r, import_snapshot=import_dir), 50 | image_file=image_file, 51 | tmp_dir=tmp_dir, 52 | ) 53 | 54 | # Delete *old* container-name: 55 | if btrfs.is_subvolume(container_dir): 56 | btrfs.delete_subvolume(container_dir) 57 | 58 | # Copy over container filesystem: 59 | btrfs.create_snapshot(import_dir, container_dir, read_only=not read_write) 60 | 61 | finally: 62 | btrfs.delete_subvolume(import_dir) 63 | 64 | return result 65 | 66 | def setup_subparser(self, subparser: typing.Any) -> None: 67 | subparser.add_argument( 68 | "--container-name", 69 | dest="override_system_name", 70 | action="store", 71 | nargs="?", 72 | default="", 73 | help="Container name to use " 74 | '[default: system-name without "system-" prefix]', 75 | ) 76 | subparser.add_argument( 77 | "--machines-dir", 78 | dest="machines_dir", 79 | action="store", 80 | default="/var/lib/machines", 81 | help="Machines directory [default: /var/lib/machines]", 82 | ) 83 | subparser.add_argument( 84 | "--read-write", 85 | dest="read_write", 86 | action="store_true", 87 | default=False, 88 | help="Make final snapshot read/write [default is read-only].", 89 | ) 90 | -------------------------------------------------------------------------------- /cleanroom/commands/_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """_test command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | from cleanroom.command import Command 8 | from cleanroom.helper.run import run, report_completed_process 9 | from cleanroom.location import Location 10 | from cleanroom.printer import debug, fail, h2, info, msg, success, trace 11 | from cleanroom.systemcontext import SystemContext 12 | 13 | import os 14 | import os.path 15 | import typing 16 | 17 | 18 | def _environment(system_context: SystemContext) -> typing.Mapping[str, str]: 19 | """Generate environment for the system tests.""" 20 | result = {k: str(v) for k, v in system_context.substitutions.items()} 21 | result["PATH"] = "/usr/bin" 22 | return result 23 | 24 | 25 | def _find_tests(system_context: SystemContext) -> typing.Generator[str, None, None]: 26 | """Find tests to run.""" 27 | tests_directory = system_context.system_tests_directory 28 | debug(f'Searching for tests in "{tests_directory}".') 29 | 30 | for f in sorted(os.listdir(tests_directory)): 31 | test = os.path.join(tests_directory, f) 32 | if not os.path.isfile(test): 33 | trace(f'"{test}": Not a file, skipping.') 34 | continue 35 | if not os.access(test, os.X_OK): 36 | trace(f'"{test}": Not executable, skipping.') 37 | continue 38 | 39 | info(f'Found test: "{test}"') 40 | yield test 41 | 42 | 43 | class TestCommand(Command): 44 | """The _test Command.""" 45 | 46 | def __init__(self, **services: typing.Any) -> None: 47 | """Constructor.""" 48 | super().__init__( 49 | "_test", 50 | help_string="Implicitly run to test images.\n\n" 51 | "Note: Will run all executable files in the " 52 | '"test" subdirectory of the systems directory and ' 53 | "will pass the system name as first argument.", 54 | file=__file__, 55 | **services, 56 | ) 57 | 58 | def validate( 59 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 60 | ) -> None: 61 | self._validate_no_arguments(location, *args, **kwargs) 62 | 63 | def __call__( 64 | self, 65 | location: Location, 66 | system_context: SystemContext, 67 | *args: typing.Any, 68 | **kwargs: typing.Any, 69 | ) -> None: 70 | """Execute command.""" 71 | h2( 72 | f'Running tests for system "{system_context.system_name}"', verbosity=2, 73 | ) 74 | env = _environment(system_context) 75 | 76 | for test in _find_tests(system_context): 77 | trace(f"{system_context.system_name}::Running test {test}...") 78 | test_result = run( 79 | test, 80 | system_context.system_name, 81 | env=env, 82 | returncode=None, 83 | work_directory=system_context.fs_directory, 84 | ) 85 | if test_result.returncode == 0: 86 | success( 87 | f'{system_context.system_name}::Test "{test}"', verbosity=3, 88 | ) 89 | else: 90 | report_completed_process(msg, test_result) 91 | fail(f'{system_context.system_name}::Test "{test}"') 92 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a set of example systems 2 | 3 | # Setup 4 | 5 | You will need a folder or subvolume on a BTRFS volume somewhere to proceed. 6 | 7 | This is your base folder. 8 | 9 | ## Work directory setup 10 | 11 | Create a new folder 'work' in your base folder. 12 | 13 | That will be your work directory going forward. 14 | 15 | Clrm *should* not write outside of the work directory, but please run 16 | the whole thing in a VM or a container to make sure it really does not 17 | break anything on your system! 18 | 19 | ## Borg repository setup 20 | 21 | Create a new borg repository in your base folder. The name must be 22 | 'borg_repository' for the examples to work. 23 | 24 | ``` 25 | borg init borg_repository --encryption=authenticated 26 | ``` 27 | 28 | Give 'foobar' as a passphrase (or whatever else you want) 29 | 30 | Then export that passphrase to the environment. 31 | 32 | ``` 33 | export BORG_PASSPHRASE=foobar 34 | ``` 35 | 36 | ## Configuration 37 | 38 | Check `type-base/pacstrap.conf` and adjust download servers used, 39 | etc. to your liking. 40 | 41 | ## Notes 42 | 43 | Clrm *should* not write outside of the work directory, but please run 44 | the whole thing in a VM or a container to make sure it really does not 45 | break anything on your system! 46 | 47 | 48 | # Creating an example image 49 | 50 | Run the following command as root: 51 | 52 | ``` 53 | export CLRM_BASE=/absolute/path/to/your/clrm-checkout 54 | export BASE_DIR=/absolute/path/to/your/base-folder 55 | 56 | "${CLRM_BASE}/clrm \ 57 | --systems-directory="${CLRM_BASE}/examples" \ 58 | --work-directory="${BASE_DIR}/work" \ 59 | --clear-storage \ 60 | --clear-scratch-directory \ 61 | --repository-base-directory="${BASE_DIR}" \ 62 | system-example 63 | ``` 64 | 65 | This command will take a while: It will do a cleanroom installation 66 | of arch linux according to the system-example definition file. 67 | 68 | Feel free to throw in up to four '--verbose' if you want to see lots of 69 | text scroll by. 70 | 71 | Leave out '--clear-storage' to keep successfully created systems between 72 | clrm runs. This can greatly speed up debugging of system definitions. 73 | 74 | Once this command is complete, you should have a system image in the 75 | borg repository you have set up earlier. 76 | 77 | Test with: 78 | ``` 79 | borg list borg_repository 80 | ``` 81 | 82 | There should be one entry starting with 'system-example-' and a recent timestamp. 83 | 84 | # Test the image 85 | 86 | Extract the image from borg and start it in qemu. 87 | 88 | Either fix permissions on the borg repository to allow access for your normal user 89 | or make sure that root can start UI applications for this to work: 90 | 91 | ``` 92 | export BORG_PASSPHRASE=foobar 93 | "${CLRM_BASE}/firestarter" \ 94 | --repository="${BASE_DIR}/borg_repository \ 95 | system-example qemu-image 96 | ``` 97 | 98 | Log in as root user using password root1234 99 | 100 | # Where to go from here 101 | 102 | Write your own system definition files based on those found here:-) 103 | 104 | "${CLRM_BASE}/clrm --list-commands 105 | 106 | should list all the pre-defined commands at your disposal. 107 | 108 | Firestarter has several export options for the images stored in borg. 109 | -------------------------------------------------------------------------------- /tests/test_helper_disk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Test for the disk helper module. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import pytest # type: ignore 9 | 10 | import os 11 | import sys 12 | 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 14 | 15 | import cleanroom.helper.disk as disk 16 | 17 | 18 | @pytest.mark.parametrize( 19 | ("input_size", "output_size"), 20 | [ 21 | pytest.param(1, 1, id="int 1"), 22 | pytest.param(0, 0, id="0"), 23 | pytest.param(1024, 1024, id="int 1024"), 24 | pytest.param("9", 9, id="9 as string"), 25 | pytest.param("1024", 1024, id="1024 as string"), 26 | pytest.param("9b", 9, id="9b"), 27 | pytest.param("9K", 9216, id="9K"), 28 | pytest.param("9k", 9216, id="9k"), 29 | pytest.param("9m", 9437184, id="9m"), 30 | pytest.param("9M", 9437184, id="9M"), 31 | pytest.param("9g", 9663676416, id="9g"), 32 | pytest.param("9G", 9663676416, id="9G"), 33 | pytest.param("9t", 9895604649984, id="9t"), 34 | pytest.param("9T", 9895604649984, id="9T"), 35 | ], 36 | ) 37 | def test_disk_byte_size(input_size, output_size) -> None: 38 | """Test absolute input file name.""" 39 | result = disk.byte_size(input_size) 40 | 41 | assert result == output_size 42 | 43 | 44 | # Error cases: 45 | @pytest.mark.parametrize( 46 | "input_size", 47 | [ 48 | pytest.param("12,3", id="float"), 49 | pytest.param("-12", id="negative int"), 50 | pytest.param("12.3b", id="float b"), 51 | pytest.param("test", id="test"), 52 | pytest.param("12z", id="wrong unit"), 53 | ], 54 | ) 55 | def test_disk_byte_size_errors(input_size) -> None: 56 | """Test absolute input file name.""" 57 | with pytest.raises(ValueError): 58 | disk.byte_size(input_size) 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "input_size", 63 | [ 64 | pytest.param(1), 65 | pytest.param(512), 66 | pytest.param(1024), 67 | pytest.param(9 * 1024), 68 | pytest.param(1 * 1024 * 1024), 69 | ], 70 | ) 71 | def test_create_image_file(tmpdir, input_size: int) -> None: 72 | if os.geteuid() != 0: 73 | pytest.skip("This test needs root to run.") 74 | 75 | file = os.path.join(tmpdir, "testfile") 76 | disk.create_image_file(file, input_size, disk_format="raw") 77 | 78 | # qemu-img does some rounding to sector sizes: 79 | assert input_size <= os.path.getsize(file) 80 | assert input_size + 1024 > os.path.getsize(file) 81 | 82 | 83 | def test_partitioner(tmpdir) -> None: 84 | if os.geteuid() != 0: 85 | pytest.skip("This test needs root to run.") 86 | 87 | with disk.NbdDevice.new_image_file( 88 | os.path.join(tmpdir, "testdisk"), disk.byte_size("512m") 89 | ) as device: 90 | partitioner = disk.Partitioner(device) 91 | assert not partitioner.is_partitioned() 92 | assert partitioner.label() is None 93 | 94 | print(f"LBA: {partitioner.first_lba()}-{partitioner.last_lba}") 95 | 96 | parts = [ 97 | disk.Partitioner.efi_partition(size="64M"), 98 | disk.Partitioner.swap_partition(size="128M"), 99 | disk.Partitioner.data_partition(name="PV0 of vg_something"), 100 | ] 101 | partitioner.repartition(parts) 102 | 103 | assert partitioner.is_partitioned() 104 | assert partitioner.label() == "gpt" 105 | -------------------------------------------------------------------------------- /cleanroom/helper/btrfs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Helpers for btrfs. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from ..printer import trace 9 | from .run import run 10 | 11 | import os 12 | import typing 13 | 14 | 15 | class BtrfsHelper: 16 | def __init__(self, btrfs_command: str): 17 | assert btrfs_command 18 | self._command = btrfs_command 19 | 20 | def create_subvolume(self, directory: str) -> None: 21 | """Create a new subvolume.""" 22 | trace(f"BTRFS: Create subvolume {directory}.") 23 | run(self._command, "subvolume", "create", directory, trace_output=trace) 24 | 25 | def set_property(self, object: str, *, name: str, value: str) -> None: 26 | """Create a new subvolume.""" 27 | trace(f"BTRFS: Set property {name} to {value} on {object}.") 28 | run(self._command, "property", "set", object, name, value, trace_output=trace) 29 | 30 | def create_snapshot( 31 | self, source: str, destination: str, *, read_only: bool = False 32 | ) -> None: 33 | """Create a new snapshot.""" 34 | extra_args: typing.Tuple[str, ...] = () 35 | extra_args = (*extra_args, "-r") if read_only else extra_args 36 | 37 | trace( 38 | f'BTRFS: Create snapshot of {source} into {destination} ({"ro" if read_only else "rw"}).' 39 | ) 40 | run( 41 | self._command, 42 | "subvolume", 43 | "snapshot", 44 | *extra_args, 45 | source, 46 | destination, 47 | trace_output=trace, 48 | ) 49 | 50 | def delete_subvolume(self, directory: str) -> bool: 51 | """Delete a subvolume.""" 52 | trace(f"BTRFS: Delete subvolume {directory}.") 53 | return ( 54 | run( 55 | self._command, 56 | "subvolume", 57 | "delete", 58 | directory, 59 | returncode=None, 60 | trace_output=None, 61 | ).returncode 62 | == 0 63 | ) 64 | 65 | def delete_subvolume_recursive(self, directory: str) -> None: 66 | """Delete all subvolumes in a subvolume or directory.""" 67 | for f in os.listdir(directory): 68 | child = os.path.join(directory, f) 69 | if os.path.isdir(child): 70 | self.delete_subvolume_recursive(child) 71 | 72 | if self.is_subvolume(directory): 73 | self.delete_subvolume(directory) 74 | 75 | def is_subvolume(self, directory: str) -> bool: 76 | """Check whether a subdirectory is a subvolume or snapshot.""" 77 | if not os.path.isdir(directory): 78 | return False 79 | return ( 80 | run( 81 | self._command, 82 | "subvolume", 83 | "show", 84 | directory, 85 | returncode=None, 86 | trace_output=None, 87 | ).returncode 88 | == 0 89 | ) 90 | 91 | def is_btrfs_filesystem(self, directory: str) -> bool: 92 | if not os.path.isdir(directory): 93 | return False 94 | return ( 95 | run( 96 | self._command, 97 | "subvolume", 98 | "list", 99 | directory, 100 | returncode=None, 101 | trace_output=None, 102 | ).returncode 103 | == 0 104 | ) 105 | -------------------------------------------------------------------------------- /cleanroom/commands/pkg_glusterfs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pkg_glusterfs command. 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | from cleanroom.command import Command 9 | from cleanroom.location import Location 10 | from cleanroom.systemcontext import SystemContext 11 | 12 | import textwrap 13 | import typing 14 | 15 | 16 | class PkgGlusterfsCommand(Command): 17 | """The pkg_glusterfs command.""" 18 | 19 | def __init__(self, **services: typing.Any) -> None: 20 | """Constructor.""" 21 | super().__init__( 22 | "pkg_glusterfs", help_string="Setup glusterfs.", file=__file__, **services 23 | ) 24 | 25 | def validate( 26 | self, location: Location, *args: typing.Any, **kwargs: typing.Any 27 | ) -> None: 28 | """Validate the arguments.""" 29 | self._validate_no_arguments(location, *args, **kwargs) 30 | 31 | def __call__( 32 | self, 33 | location: Location, 34 | system_context: SystemContext, 35 | *args: typing.Any, 36 | **kwargs: typing.Any 37 | ) -> None: 38 | """Execute command.""" 39 | self._execute( 40 | location, system_context, "pacman", "glusterfs", "grep", "python3" 41 | ) 42 | 43 | self._execute( 44 | location.next_line(), 45 | system_context, 46 | "create", 47 | "/usr/lib/tmpfiles.d/mnt-gluster.conf", 48 | textwrap.dedent( 49 | """\ 50 | d /mnt/gluster 0700 root root - - 51 | d /mnt/gluster/0 0755 root root - - 52 | d /mnt/gluster/1 0755 root root - - 53 | d /mnt/gluster/2 0755 root root - - 54 | d /mnt/gluster/4 0755 root root - - 55 | """ 56 | ), 57 | mode=0o644, 58 | ) 59 | 60 | self._execute( 61 | location.next_line(), 62 | system_context, 63 | "mkdir", 64 | "/usr/lib/systemd/system/glusterd.service.d", 65 | mode=0o755, 66 | ) 67 | self._execute( 68 | location.next_line(), 69 | system_context, 70 | "create", 71 | "/usr/lib/systemd/system/glusterd.service.d/override.conf", 72 | textwrap.dedent( 73 | """\ 74 | [Service] 75 | Type=simple 76 | ExecStart= 77 | ExecStart=/usr/bin/glusterd -N --log-file=- --log-level INFO 78 | PIDFile= 79 | KillMode=control-group 80 | Environment= 81 | EnvironmentFile= 82 | StateDirectory=glusterd 83 | RuntimeDirectory=gluster 84 | LogsDirectory=glusterfs 85 | """ 86 | ), 87 | mode=0o644, 88 | ) 89 | 90 | self._execute( 91 | location.next_line(), 92 | system_context, 93 | "systemd_harden_unit", 94 | "glusterd.service", 95 | PrivateUsers=False, 96 | ) 97 | 98 | # Fix rdma usage which is not included in archlinux: 99 | self._execute( 100 | location.next_line(), 101 | system_context, 102 | "sed", 103 | "/option transport-type/ coption transport type = socket", 104 | "/etc/glusterfs/glusterd.vol", 105 | ) 106 | -------------------------------------------------------------------------------- /tests/test_helper_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Test for the cleanroom.generator.helper.generic.group 3 | 4 | @author: Tobias Hunger 5 | """ 6 | 7 | 8 | import pytest # type: ignore 9 | import typing 10 | 11 | import os 12 | import os.path 13 | import sys 14 | 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 16 | 17 | from cleanroom.binarymanager import BinaryManager, Binaries 18 | from cleanroom.helper.group import GroupHelper 19 | 20 | 21 | @pytest.mark.parametrize( 22 | ("group_name", "expected_data"), 23 | [ 24 | pytest.param( 25 | "root", 26 | {"name": "root", "password": "x", "gid": 0, "members": ["root"]}, 27 | id="root", 28 | ), 29 | pytest.param( 30 | "sys", 31 | {"name": "sys", "password": "x", "gid": 3, "members": ["bin"]}, 32 | id="sys", 33 | ), 34 | pytest.param( 35 | "mem", {"name": "mem", "password": "x", "gid": 8, "members": []}, id="sys" 36 | ), 37 | pytest.param( 38 | "test", 39 | { 40 | "name": "test", 41 | "password": "x", 42 | "gid": 10001, 43 | "members": ["test", "test1", "test2"], 44 | }, 45 | id="test", 46 | ), 47 | ], 48 | ) 49 | def test_group_data( 50 | user_setup, group_name: str, expected_data: typing.Dict[str, typing.Any] 51 | ) -> None: 52 | """Test reading of valid data from /etc/passwd-like file.""" 53 | result = GroupHelper.group_data(group_name, root_directory=user_setup) 54 | assert result 55 | assert result._asdict() == expected_data 56 | 57 | 58 | def test_missing_group_data_file(user_setup) -> None: 59 | """Test reading from an unknown /etc/group-like file.""" 60 | result = GroupHelper.group_data( 61 | "root", root_directory=os.path.join(user_setup, "etc") 62 | ) 63 | assert result is None 64 | 65 | 66 | def test_missing_group_data(user_setup) -> None: 67 | """Test reading a unknown user name from /etc/passwd-like file.""" 68 | result = GroupHelper.group_data("unknownGroup", root_directory=user_setup) 69 | assert result 70 | assert result._asdict() == { 71 | "name": "nobody", 72 | "password": "x", 73 | "gid": 65534, 74 | "members": [], 75 | } 76 | 77 | 78 | def test_add_group(user_setup) -> None: 79 | binary_manager = BinaryManager() 80 | group_helper = GroupHelper( 81 | binary_manager.binary(Binaries.GROUPADD), 82 | binary_manager.binary(Binaries.GROUPMOD), 83 | ) 84 | group_helper.groupadd("addedgroup", gid=1200, root_directory=user_setup) 85 | 86 | result = GroupHelper.group_data("addedgroup", root_directory=user_setup) 87 | assert result 88 | assert result._asdict() == { 89 | "name": "addedgroup", 90 | "password": "x", 91 | "gid": 1200, 92 | "members": [], 93 | } 94 | 95 | 96 | def test_mod_group(user_setup) -> None: 97 | binary_manager = BinaryManager() 98 | group_helper = GroupHelper( 99 | binary_manager.binary(Binaries.GROUPADD), 100 | binary_manager.binary(Binaries.GROUPMOD), 101 | ) 102 | group_helper.groupmod("test", rename="tester", root_directory=user_setup) 103 | 104 | result = GroupHelper.group_data("tester", root_directory=user_setup) 105 | assert result 106 | assert result._asdict() == { 107 | "name": "tester", 108 | "password": "x", 109 | "gid": 10001, 110 | "members": ["test", "test1", "test2"], 111 | } 112 | --------------------------------------------------------------------------------