sudo apt-get install libfdisk-dev
git clone https://github.com/systemd/systemd --depth=1
- meson setup systemd/build systemd -Drepart=true -Defi=true -Dbootloader=true -Dukify=true
+ meson setup systemd/build systemd -Drepart=true -Defi=true -Dbootloader=true -Dukify=true -Dfirstboot=true
BINARIES=(
bootctl
systemctl
systemd-analyze
systemd-dissect
+ systemd-firstboot
systemd-nspawn
systemd-repart
ukify
: Packages are appended to the list. Packages prefixed with "!" are
removed from the list. "!\*" removes all packages from the list.
-`Password=`, `--password=`
-
-: Set the password of the `root` user. By default the `root` account
- is locked. If this option is not used, but a file `mkosi.rootpw`
- exists in the local directory, the root password is automatically
- read from it.
-
-`PasswordIsHashed=`, `--password-is-hashed`
-
-: Indicate that the password supplied for the `root` user has already been
- hashed, so that the string supplied with `Password=` or `mkosi.rootpw` will
- be written to `/etc/shadow` literally.
-
`Autologin=`, `--autologin`
: Enable autologin for the `root` user on `/dev/pts/0` (nspawn),
same as `KernelModulesInitrdInclude=` except that all modules that match any of the specified patterns are
excluded from the kernel modules initrd. This setting takes priority over `KernelModulesInitrdInclude=`.
+`Locale=`, `--locale=`,
+`LocaleMessages=`, `--locale-messages=`,
+`Keymap=`, `--keymap=`,
+`Timezone=`, `--timezone=`,
+`Hostname=`, `--hostname=`,
+`RootPassword=`, `--root-password=`,
+`RootPasswordHashed=`, `--root-password-hashed=`,
+`RootPasswordFile=`, `--root-password-file=`,
+`RootShell=`, `--root-shell=`
+
+: These settings correspond to the identically named systemd-firstboot options. See the systemd firstboot
+ [manpage](https://www.freedesktop.org/software/systemd/man/systemd-firstboot.html) for more information.
+
### [Validation] Section
`Checksum=`, `--checksum`
import base64
import contextlib
-import crypt
import datetime
import errno
import hashlib
flatten,
format_rlimit,
is_apt_distribution,
- patch_file,
prepend_to_environ_path,
tmp_dir,
)
die(f"Removing packages is not supported for {state.config.distribution}")
-def reset_machine_id(state: MkosiState) -> None:
- """Make /etc/machine-id an empty file.
-
- This way, on the next boot is either initialized and committed (if /etc is
- writable) or the image runs with a transient machine ID, that changes on
- each boot (if the image is read-only).
- """
- with complete_step("Resetting machine ID"):
- machine_id = state.root / "etc/machine-id"
- machine_id.unlink(missing_ok=True)
- machine_id.write_text("uninitialized\n")
-
-
-def reset_random_seed(root: Path) -> None:
- """Remove random seed file, so that it is initialized on first boot"""
- random_seed = root / "var/lib/systemd/random-seed"
- if not random_seed.exists():
- return
-
- with complete_step("Removing random seed"):
- random_seed.unlink()
-
-
-def configure_root_password(state: MkosiState) -> None:
- "Set the root account password, or just delete it so it's easy to log in"
-
- if state.config.password == "":
- with complete_step("Deleting root password"):
-
- def delete_root_pw(line: str) -> str:
- if line.startswith("root:"):
- return ":".join(["root", ""] + line.split(":")[2:])
- return line
-
- patch_file(state.root / "etc/passwd", delete_root_pw)
- elif state.config.password:
- with complete_step("Setting root password"):
- if state.config.password_is_hashed:
- password = state.config.password
- else:
- password = crypt.crypt(state.config.password, crypt.mksalt(crypt.METHOD_SHA512))
-
- def set_root_pw(line: str) -> str:
- if line.startswith("root:"):
- return ":".join(["root", password] + line.split(":")[2:])
- return line
-
- shadow = state.root / "etc/shadow"
- try:
- patch_file(shadow, set_root_pw)
- except FileNotFoundError:
- shadow.write_text(f"root:{password}:0:0:99999:7:::\n")
-
-
def configure_autologin(state: MkosiState) -> None:
if not state.config.autologin:
return
Script Environment: {line_join_list(env)}
Scripts with network: {yes_no(config.with_network)}
nspawn Settings: {none_to_none(config.nspawn_settings)}
- Password: {("(default)" if config.password is None else "(set)")}
+ Locale: {none_to_default(config.locale)}
+ Locale Messages: {none_to_default(config.locale_messages)}
+ Keymap: {none_to_default(config.keymap)}
+ Timezone: {none_to_default(config.timezone)}
+ Hostname: {none_to_default(config.hostname)}
+ Root Password: {("(set)" if config.root_password or config.root_password_hashed or config.root_password_file else "(default)")}
+ Root Shell: {none_to_default(config.root_shell)}
Autologin: {yes_no(config.autologin)}
{bold("HOST CONFIGURATION")}:
run(["systemd-sysusers", "--root", state.root])
-def run_preset_all(state: MkosiState) -> None:
+def run_preset(state: MkosiState) -> None:
with complete_step("Applying presets…"):
run(["systemctl", "--root", state.root, "preset-all"])
+def run_firstboot(state: MkosiState) -> None:
+ settings = (
+ ("--locale", state.config.locale),
+ ("--locale-messages", state.config.locale_messages),
+ ("--keymap", state.config.keymap),
+ ("--timezone", state.config.timezone),
+ ("--hostname", state.config.hostname),
+ ("--root-password", state.config.root_password),
+ ("--root-password-hashed", state.config.root_password_hashed),
+ ("--root-password-file", state.config.root_password_file),
+ ("--root-shell", state.config.root_shell),
+ )
+
+ options = []
+
+ for setting, value in settings:
+ if not value:
+ continue
+
+ options += [setting, value]
+
+ if not options:
+ return
+
+ with complete_step("Applying first boot settings"):
+ run(["systemd-firstboot", "--root", state.root, "--force", *options])
+
+
def run_selinux_relabel(state: MkosiState) -> None:
selinux = state.root / "etc/selinux/config"
if not selinux.exists():
if for_cache:
return
- configure_root_password(state)
configure_autologin(state)
configure_initrd(state)
run_build_script(state)
configure_ssh(state)
run_postinst_script(state)
run_sysusers(state)
- run_preset_all(state)
+ run_preset(state)
run_depmod(state)
+ run_firstboot(state)
remove_packages(state)
if manifest:
clean_package_manager_metadata(state)
remove_files(state)
- reset_machine_id(state)
- reset_random_seed(state.root)
run_finalize_script(state)
run_selinux_relabel(state)
import fnmatch
import functools
import inspect
-import logging
import operator
import os.path
import platform
required: bool = True,
absolute: bool = True,
expanduser: bool = True,
- expandvars: bool = True) -> Path:
+ expandvars: bool = True,
+ secret: bool = False) -> Path:
if expandvars:
value = os.path.expandvars(value)
if absolute:
path = path.absolute()
+ if secret:
+ mode = path.stat().st_mode & 0o777
+ if mode & 0o007:
+ die(textwrap.dedent(f"""\
+ Permissions of '{path}' of '{mode:04o}' are too open.
+ When creating secret files use an access mode that restricts access to the owner only.
+ """))
+
return path
required: bool = True,
absolute: bool = True,
expanduser: bool = True,
- expandvars: bool = True) -> Callable[[str], Path]:
+ expandvars: bool = True,
+ secret: bool = False) -> Callable[[str], Path]:
return functools.partial(
parse_path,
required=required,
absolute=absolute,
expanduser=expanduser,
expandvars=expandvars,
+ secret=secret,
)
required: bool = True,
absolute: bool = True,
expanduser: bool = True,
- expandvars: bool = True) -> ConfigParseCallback:
+ expandvars: bool = True,
+ secret: bool = False) -> ConfigParseCallback:
def config_parse_path(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[Path]:
if dest in namespace:
return getattr(namespace, dest) # type: ignore
absolute=absolute,
expanduser=expanduser,
expandvars=expandvars,
+ secret=secret,
)
return None
split_artifacts: bool
sign: bool
key: Optional[str]
- password: Optional[str]
- password_is_hashed: bool
autologin: bool
extra_search_paths: list[Path]
ephemeral: bool
acl: bool
bootable: ConfigFeature
use_subvolumes: ConfigFeature
+ locale: Optional[str]
+ locale_messages: Optional[str]
+ keymap: Optional[str]
+ timezone: Optional[str]
+ hostname: Optional[str]
+ root_password: Optional[str]
+ root_password_hashed: Optional[str]
+ root_password_file: Optional[Path]
+ root_shell: Optional[str]
# QEMU-specific options
qemu_gui: bool
parse=config_parse_feature,
match=config_make_list_matcher(delimiter=",", parse=parse_feature),
),
- MkosiConfigSetting(
- dest="password",
- section="Content",
- ),
- MkosiConfigSetting(
- dest="password_is_hashed",
- section="Content",
- parse=config_parse_boolean,
- ),
MkosiConfigSetting(
dest="autologin",
section="Content",
section="Content",
parse=config_make_list_parser(delimiter=","),
),
+ MkosiConfigSetting(
+ dest="locale",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="locale_messages",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="keymap",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="timezone",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="hostname",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="root_password",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="root_password_hashed",
+ section="Content",
+ parse=config_parse_string,
+ ),
+ MkosiConfigSetting(
+ dest="root_password_file",
+ section="Content",
+ parse=config_make_path_parser(secret=True),
+ paths=("mkosi.rootpw",),
+ ),
+ MkosiConfigSetting(
+ dest="root_shell",
+ section="Content",
+ parse=config_parse_string,
+ ),
MkosiConfigSetting(
dest="checksum",
section="Validation",
nargs="?",
action=action,
)
- group.add_argument("--password", help="Set the root password", action=action)
- group.add_argument(
- "--password-is-hashed",
- metavar="BOOL",
- help="Indicate that the root password has already been hashed",
- nargs="?",
- action=action,
- )
group.add_argument(
"--autologin",
metavar="BOOL",
metavar="REGEX",
action=action,
)
+ group.add_argument(
+ "--locale",
+ help="Set the system locale",
+ metavar="LOCALE",
+ action=action,
+ )
+ group.add_argument(
+ "--locale-messages",
+ help="Set the messages locale",
+ metavar="LOCALE",
+ action=action,
+ )
+ group.add_argument(
+ "--keymap",
+ help="Set the system keymap",
+ metavar="KEYMAP",
+ action=action,
+ )
+ group.add_argument(
+ "--timezone",
+ help="Set the system timezone",
+ metavar="TIMEZONE",
+ action=action,
+ )
+ group.add_argument(
+ "--hostname",
+ help="Set the system hostname",
+ metavar="HOSTNAME",
+ action=action,
+ )
+ group.add_argument(
+ "--root-password",
+ help="Set the system root password",
+ metavar="PASSWORD",
+ action=action,
+ )
+ group.add_argument(
+ "--root-password-hashed",
+ help="Set the system root password (hashed)",
+ metavar="PASSWORD-HASHED",
+ action=action,
+ )
+ group.add_argument(
+ "--root-password-file",
+ help="Set the system root password (file)",
+ metavar="PATH",
+ action=action,
+ )
+ group.add_argument(
+ "--root-shell",
+ help="Set the system root shell",
+ metavar="SHELL",
+ action=action,
+ )
group = parser.add_argument_group("Validation options")
group.add_argument(
pass
-def require_private_file(name: Path, description: str) -> None:
- mode = os.stat(name).st_mode & 0o777
- if mode & 0o007:
- logging.warning(textwrap.dedent(f"""\
- Permissions of '{name}' of '{mode:04o}' are too open.
- When creating {description} files use an access mode that restricts access to the owner only.
- """))
-
-
-def find_password(args: argparse.Namespace) -> None:
- if args.password is not None:
- return
-
- try:
- pwfile = Path("mkosi.rootpw")
- require_private_file(pwfile, "root password")
-
- args.password = pwfile.read_text().strip()
-
- except FileNotFoundError:
- pass
-
-
def load_credentials(args: argparse.Namespace) -> dict[str, str]:
creds = {}
if args.sign_expected_pcr is None:
args.sign_expected_pcr = bool(shutil.which("systemd-measure"))
- # Resolve passwords late so we can accurately determine whether a build is needed
- find_password(args)
-
if args.repo_dirs and not (
is_dnf_distribution(args.distribution)
or is_apt_distribution(args.distribution)
]
if apivfs:
+ if not apivfs.joinpath("etc/machine-id").exists():
+ # Uninitialized means we want it to get initialized on first boot.
+ apivfs.joinpath("etc/machine-id").write_text("uninitialized\n")
+ apivfs.chmod(0o0444)
+
cmdline += [
"--tmpfs", apivfs / "run",
"--tmpfs", apivfs / "tmp",
with tempfile.TemporaryDirectory(dir="/var/tmp", prefix="mkosi-var-tmp") as var_tmp:
if apivfs:
- cmdline += ["--bind", var_tmp, apivfs / "var/tmp"]
+ cmdline += [
+ "--bind", var_tmp, apivfs / "var/tmp",
+ # Make sure /etc/machine-id is not overwritten by any package manager post install scripts.
+ "--ro-bind", apivfs / "etc/machine-id", apivfs / "etc/machine-id",
+ ]
cmdline += ["sh", "-c"]
template = f"{chmod} && exec {{}} || exit $?"
try:
- return run([*cmdline, template.format(shlex.join(str(s) for s in cmd))],
- text=True, stdout=stdout, env=env, log=False)
+ result = run([*cmdline, template.format(shlex.join(str(s) for s in cmd))],
+ text=True, stdout=stdout, env=env, log=False)
except subprocess.CalledProcessError as e:
logging.error(f'"{shlex.join(str(s) for s in cmd)}" returned non-zero exit code {e.returncode}.')
if ARG_DEBUG_SHELL.get():
run([*cmdline, template.format("sh")], stdin=sys.stdin, check=False, env=env, log=False)
raise e
+ # Clean up some stuff that might get written by package manager post install scripts.
+ if apivfs:
+ for f in ("var/lib/systemd/random-seed", "var/lib/systemd/credential.secret", "etc/machine-info"):
+ apivfs.joinpath(f).unlink(missing_ok=True)
+
+ return result
+
def run_workspace_command(
root: Path,
import pwd
import re
import resource
-import shutil
import sys
import tempfile
from collections.abc import Iterable, Iterator, Sequence
return Path(path)
-def patch_file(filepath: Path, line_rewriter: Callable[[str], str]) -> None:
- temp_new_filepath = filepath.with_suffix(filepath.suffix + ".tmp.new")
-
- with filepath.open("r") as old, temp_new_filepath.open("w") as new:
- for line in old:
- new.write(line_rewriter(line))
-
- shutil.copystat(filepath, temp_new_filepath)
- os.remove(filepath)
- shutil.move(temp_new_filepath, filepath)
-
-
def sort_packages(packages: Iterable[str]) -> list[str]:
"""Sorts packages: normal first, paths second, conditional third"""