feat: initial commit
This commit is contained in:
commit
8085c747cb
14 changed files with 926 additions and 0 deletions
0
usr/lib/akshara/__init__.py
Normal file
0
usr/lib/akshara/__init__.py
Normal file
116
usr/lib/akshara/akshara
Executable file
116
usr/lib/akshara/akshara
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import fasteners
|
||||
import yaml
|
||||
from utils.gen_rootfs import gen_rootfs
|
||||
from utils.helpers import (
|
||||
get_system_config,
|
||||
is_already_latest,
|
||||
notify_prompt,
|
||||
output,
|
||||
resolve_config,
|
||||
)
|
||||
from utils.update import update
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""Handles system operations."""
|
||||
|
||||
|
||||
@cli.command("gen-rootfs")
|
||||
@click.argument("config_file", type=click.File("r"))
|
||||
@click.argument(
|
||||
"rootfs_path", type=click.Path(exists=True, file_okay=False, dir_okay=True)
|
||||
)
|
||||
def cmd_gen_rootfs(config_file, rootfs_path):
|
||||
"""Generates a rootfs using a provided configuration file."""
|
||||
gen_rootfs(resolve_config(yaml.safe_load(config_file.read())), rootfs_path)
|
||||
|
||||
|
||||
@cli.command("update-check", hidden=True)
|
||||
def update_check_daemon():
|
||||
if os.environ.get("USER") == "gdm-greeter":
|
||||
exit()
|
||||
|
||||
check_interval = 3600
|
||||
|
||||
if isinstance(get_system_config().get("auto-update"), dict):
|
||||
if not get_system_config()["auto-update"]["enabled"]:
|
||||
exit()
|
||||
if isinstance(get_system_config()["auto-update"].get("interval"), int):
|
||||
check_interval = get_system_config()["auto-update"]["interval"]
|
||||
|
||||
while True:
|
||||
try:
|
||||
if not os.path.isdir("/.update_rootfs"):
|
||||
if not is_already_latest():
|
||||
if notify_prompt(
|
||||
title="Update available",
|
||||
body="A system update is available",
|
||||
actions={"update": "Update in the background"},
|
||||
):
|
||||
if (
|
||||
subprocess.run(["pkexec", "akshara", "update"]).returncode
|
||||
== 0
|
||||
):
|
||||
if (
|
||||
notify_prompt(
|
||||
title="System updated",
|
||||
body="Reboot to apply update?",
|
||||
actions={"reboot": "Reboot now", "later": "Later"},
|
||||
)
|
||||
== "reboot"
|
||||
):
|
||||
subprocess.run(["reboot"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
time.sleep(check_interval)
|
||||
|
||||
|
||||
@cli.command("update")
|
||||
@click.option("-f", "--force", is_flag=True)
|
||||
def update_cmd(force):
|
||||
"""
|
||||
Update your system.
|
||||
"""
|
||||
|
||||
if os.geteuid() != 0:
|
||||
output.error("must be run as root")
|
||||
exit(1)
|
||||
|
||||
Path("/var/lib/akshara").mkdir(parents=True, exist_ok=True)
|
||||
system_lock = fasteners.InterProcessLock("/var/lib/akshara/.system-lock")
|
||||
output.info("attempting to acquire system lock")
|
||||
output.info(
|
||||
"if stuck at this line for long, an update may be progressing in the background"
|
||||
)
|
||||
|
||||
with system_lock:
|
||||
print()
|
||||
output.info("checking if already up-to-date...")
|
||||
|
||||
if (not os.path.isdir("/.update_rootfs")) or force:
|
||||
if is_already_latest() and not force:
|
||||
output.info("your system is already up-to-date")
|
||||
sys.exit(0)
|
||||
else:
|
||||
output.error(
|
||||
"an update has already been downloaded and is waiting to be applied"
|
||||
)
|
||||
output.error("you must reboot before running this command")
|
||||
sys.exit(1)
|
||||
|
||||
update()
|
||||
output.info("update complete; you may now reboot.")
|
||||
|
||||
|
||||
cli()
|
||||
0
usr/lib/akshara/classes/__init__.py
Normal file
0
usr/lib/akshara/classes/__init__.py
Normal file
119
usr/lib/akshara/classes/rootfs.py
Normal file
119
usr/lib/akshara/classes/rootfs.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class RootFS:
|
||||
"""Handles root filesystem operations.
|
||||
|
||||
Attributes:
|
||||
rootfs_path: A string containing the path to the root filesystem
|
||||
"""
|
||||
|
||||
def __init__(self, rootfs_path: str, distro_config: dict) -> None:
|
||||
"""Initialises an instance based on a rootfs path and distro configuration.
|
||||
|
||||
Args:
|
||||
rootfs: Path to root filesystem.
|
||||
distro_config: Dictionary containing distro configuration.
|
||||
"""
|
||||
self.rootfs_path = rootfs_path
|
||||
self.distro_config = distro_config
|
||||
|
||||
def exists(self, path):
|
||||
"""Checks if path exists within rootfs.
|
||||
|
||||
Args:
|
||||
path: Path to check for.
|
||||
"""
|
||||
return os.path.exists(os.path.join(self.rootfs_path, path))
|
||||
|
||||
def init(self) -> subprocess.CompletedProcess:
|
||||
"""Initialise rootfs."""
|
||||
|
||||
# Create blendOS cache directory
|
||||
Path("/var/cache/blendOS").mkdir(parents=True, exist_ok=True)
|
||||
Path(os.path.join(self.rootfs_path, "var/cache/blendOS")).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
# subprocess.run(
|
||||
# ["rm", "-rf", "--", os.path.join(self.rootfs_path, "var/cache/blendOS")]
|
||||
# )
|
||||
# subprocess.run(
|
||||
# [
|
||||
# "cp",
|
||||
# "-a",
|
||||
# "--",
|
||||
# "/var/cache/blendOS",
|
||||
# os.path.join(self.rootfs_path, "var/cache/blendOS"),
|
||||
# ]
|
||||
# )
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"mount",
|
||||
"--bind",
|
||||
"/var/cache/blendOS",
|
||||
os.path.join(self.rootfs_path, "var/cache/blendOS"),
|
||||
]
|
||||
)
|
||||
|
||||
completedProcess = subprocess.run(
|
||||
["bash", "-s"],
|
||||
text=True,
|
||||
input=self.distro_config["before-stages"],
|
||||
cwd=self.rootfs_path,
|
||||
)
|
||||
|
||||
subprocess.run(["umount", os.path.join(self.rootfs_path, "var/cache/blendOS")])
|
||||
|
||||
return completedProcess
|
||||
|
||||
def exec(self, cmd, **kwargs) -> subprocess.CompletedProcess:
|
||||
"""Runs command within rootfs.
|
||||
|
||||
Args:
|
||||
cmd: List comprising command and arguments.
|
||||
**kwargs: Keyword arguments list for subprocess.run().
|
||||
"""
|
||||
return subprocess.run(
|
||||
[
|
||||
"systemd-nspawn",
|
||||
"--quiet",
|
||||
"--pipe",
|
||||
"--bind=/var/cache/blendOS",
|
||||
"-D",
|
||||
self.rootfs_path,
|
||||
]
|
||||
+ list(cmd),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# def copy_kernels_to_boot(self) -> None:
|
||||
# """Copies any found kernels to /boot within rootfs."""
|
||||
# kernels = [
|
||||
# kernel
|
||||
# for kernel in os.listdir(f"{self.rootfs_path}/usr/lib/modules")
|
||||
# if self.exists(f"/usr/lib/modules/{kernel}/vmlinuz")
|
||||
# ]
|
||||
|
||||
# if len(kernels) == 0:
|
||||
# return
|
||||
|
||||
# for boot_file in os.listdir(f"{self.rootfs_path}/boot"):
|
||||
# if not os.path.isdir(boot_file):
|
||||
# self.exec(["rm", "-f", f"/boot/{boot_file}"])
|
||||
|
||||
# for kernel in kernels:
|
||||
# self.exec(
|
||||
# ["cp", f"/usr/lib/modules/{kernel}/vmlinuz", f"/boot/vmlinuz-{kernel}"],
|
||||
# stdout=subprocess.DEVNULL,
|
||||
# stderr=subprocess.DEVNULL,
|
||||
# )
|
||||
|
||||
# def gen_initramfs(self) -> None:
|
||||
# """Generates initramfs within rootfs."""
|
||||
# self.exec(["dracut", "--force", "--regenerate-all"])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.rootfs_path
|
||||
0
usr/lib/akshara/utils/__init__.py
Normal file
0
usr/lib/akshara/utils/__init__.py
Normal file
43
usr/lib/akshara/utils/gen_rootfs.py
Normal file
43
usr/lib/akshara/utils/gen_rootfs.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
from classes.rootfs import RootFS
|
||||
|
||||
from . import output
|
||||
|
||||
|
||||
def run_script_rootfs(rootfs: RootFS, input: str, args: list):
|
||||
"""Run a script within the rootfs."""
|
||||
|
||||
rootfs.exec(["bash", "-s", *args], text=True, input=input)
|
||||
|
||||
|
||||
def gen_rootfs(system_config: dict, rootfs_path: str) -> RootFS:
|
||||
"""Generates a rootfs for a given system configuration."""
|
||||
|
||||
rootfs = RootFS(rootfs_path, system_config["distro-config"])
|
||||
rootfs.init()
|
||||
|
||||
modules = {}
|
||||
|
||||
if isinstance(system_config.get("modules"), list):
|
||||
for module in system_config["modules"]:
|
||||
modules[module["name"]] = module["run"]
|
||||
|
||||
if isinstance(system_config.get("stages"), list):
|
||||
for stage in system_config["stages"]:
|
||||
if stage.get("module") not in modules.keys():
|
||||
output.error(f"{stage.get('module')} not found within module list.")
|
||||
exit(1)
|
||||
|
||||
inputs = stage["inputs"] if isinstance(stage.get("inputs"), list) else []
|
||||
run_script_rootfs(rootfs, modules[stage["module"]], inputs)
|
||||
|
||||
if isinstance(system_config["distro-config"].get("after-stages"), str):
|
||||
run_script_rootfs(rootfs, system_config["distro-config"]["after-stages"], [])
|
||||
|
||||
with open(os.path.join(rootfs_path, "usr/system.json"), "w") as system_json_file:
|
||||
json.dump(system_config, system_json_file, ensure_ascii=False)
|
||||
pass
|
||||
|
||||
return rootfs
|
||||
129
usr/lib/akshara/utils/helpers.py
Normal file
129
usr/lib/akshara/utils/helpers.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from . import output
|
||||
|
||||
|
||||
def resolve_config(system_config: dict) -> dict:
|
||||
if system_config["track"] == "base":
|
||||
try:
|
||||
return {
|
||||
"track": "base",
|
||||
"modules": system_config["modules"]
|
||||
if isinstance(system_config.get("modules"), list)
|
||||
else [],
|
||||
"stages": system_config["stages"]
|
||||
if isinstance(system_config.get("stages"), list)
|
||||
else [],
|
||||
"distro-config": system_config["distro-config"],
|
||||
"auto-update": system_config["auto-update"]
|
||||
if isinstance(system_config.get("auto-update"), dict)
|
||||
else {"enabled": False},
|
||||
}
|
||||
except IndexError:
|
||||
output.error("base track must include distro-config")
|
||||
exit(1)
|
||||
else:
|
||||
base_track_src = system_config["track"]
|
||||
if os.path.exists(base_track_src):
|
||||
base_config = resolve_config(yaml.safe_load(base_track_src))
|
||||
elif base_track_src.startswith("http:") or base_track_src.startswith("https:"):
|
||||
base_config = resolve_config(
|
||||
yaml.safe_load(
|
||||
requests.get(base_track_src, allow_redirects=True).content.decode()
|
||||
)
|
||||
)
|
||||
pass
|
||||
else:
|
||||
output.error(f"could not resolve parent track - {system_config['track']}")
|
||||
exit(1)
|
||||
|
||||
base_config["modules"] += (
|
||||
system_config["modules"]
|
||||
if isinstance(system_config.get("modules"), list)
|
||||
else []
|
||||
)
|
||||
|
||||
base_config["stages"] += (
|
||||
system_config["stages"]
|
||||
if isinstance(system_config.get("stages"), list)
|
||||
else []
|
||||
)
|
||||
|
||||
base_config["auto-update"] = (
|
||||
system_config["auto-update"]
|
||||
if isinstance(system_config.get("auto-update"), dict)
|
||||
else base_config["auto-update"]
|
||||
)
|
||||
|
||||
return base_config
|
||||
|
||||
|
||||
def get_system_config():
|
||||
if os.path.exists("/system.yaml"):
|
||||
with open("/system.yaml") as system_yaml_file:
|
||||
system_config = yaml.safe_load(system_yaml_file.read())
|
||||
elif os.path.exists("/system.yml"):
|
||||
with open("/system.yml") as system_yml_file:
|
||||
system_config = yaml.safe_load(system_yml_file.read())
|
||||
elif os.path.exists("/system.json"):
|
||||
with open("/system.json") as system_json_file:
|
||||
system_config = yaml.safe_load(system_json_file.read())
|
||||
else:
|
||||
output.error("no system config file found")
|
||||
exit(1)
|
||||
|
||||
return resolve_config(system_config)
|
||||
|
||||
|
||||
def is_already_latest() -> bool:
|
||||
"""Checks if system is already up-to-date.
|
||||
|
||||
Returns:
|
||||
True if up-to-date, else False.
|
||||
"""
|
||||
|
||||
if os.path.isfile("/usr/system.json"):
|
||||
with open("/usr/system.json") as system_json_file:
|
||||
contents = system_json_file.read().strip()
|
||||
if (
|
||||
json.dumps(get_system_config(), ensure_ascii=False).strip()
|
||||
== contents.strip()
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def notify_prompt(title: str, body: str, actions: dict) -> str:
|
||||
"""Display a notification prompting the user.
|
||||
|
||||
Args:
|
||||
title: Title of the notification.
|
||||
body: Body of the notification.
|
||||
actions: Dict containing action key-value pairs.
|
||||
|
||||
Returns:
|
||||
String containing selected action.
|
||||
"""
|
||||
return (
|
||||
subprocess.run(
|
||||
[
|
||||
"notify-send",
|
||||
"--app-name=System",
|
||||
"--urgency=critical",
|
||||
title,
|
||||
body,
|
||||
*[f"--action={action}={actions[action]}" for action in actions.keys()],
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
.stdout.decode()
|
||||
.strip()
|
||||
)
|
||||
27
usr/lib/akshara/utils/output.py
Normal file
27
usr/lib/akshara/utils/output.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import sys
|
||||
|
||||
|
||||
def info(msg: str) -> None:
|
||||
"""Print an informative message to stdout.
|
||||
|
||||
Args:
|
||||
msg: String containing the message.
|
||||
"""
|
||||
print(f"I: {msg}")
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
"""Print a warning message to stderr.
|
||||
|
||||
Args:
|
||||
msg: String containing the message."""
|
||||
print(f"W: {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def error(msg: str) -> None:
|
||||
"""Print an error message to stderr.
|
||||
|
||||
Args:
|
||||
msg: String containing the message.
|
||||
"""
|
||||
print(f"E: {msg}", file=sys.stderr)
|
||||
224
usr/lib/akshara/utils/update.py
Normal file
224
usr/lib/akshara/utils/update.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import filecmp
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from classes.rootfs import RootFS
|
||||
from utils import output, users
|
||||
|
||||
from . import helpers
|
||||
from .gen_rootfs import gen_rootfs
|
||||
|
||||
|
||||
def update_cleanup() -> None:
|
||||
"""Clean-up from previous rebase/update."""
|
||||
subprocess.run(
|
||||
["umount", "-l", "/var/cache/akshara/rootfs/var/cache/blendOS"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"rm",
|
||||
"-rf",
|
||||
"/var/cache/akshara/rootfs",
|
||||
"/.update_rootfs",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def merge_etc(new_rootfs: RootFS, overrides_keep_new: dict) -> None:
|
||||
"""Merges /etc trees.
|
||||
|
||||
Args:
|
||||
new_rootfs: New RootFS instance.
|
||||
overrides_keep_new: Dictionary comprising overrides and whether to keep new.
|
||||
"""
|
||||
subprocess.run(["cp", "-ax", f"{new_rootfs}/etc", f"{new_rootfs}/usr/etc"])
|
||||
|
||||
if not os.path.isdir("/usr/etc"):
|
||||
subprocess.run(["rm", "-rf", "/usr/etc"])
|
||||
subprocess.run(["cp", "-ax", "/etc", "/usr/etc"])
|
||||
|
||||
etc_diff = filecmp.dircmp("/etc/", "/usr/etc/")
|
||||
|
||||
def handle_diff_etc_files(dcmp):
|
||||
dir_name = dcmp.left.replace("/etc/", f"{new_rootfs}/etc/", 1)
|
||||
subprocess.run(["mkdir", "-p", dir_name])
|
||||
for name in dcmp.left_only:
|
||||
subprocess.run(["cp", "-ax", "--", os.path.join(dcmp.left, name), dir_name])
|
||||
for name in dcmp.diff_files:
|
||||
subprocess.run(["cp", "-ax", "--", os.path.join(dcmp.left, name), dir_name])
|
||||
for sub_dcmp in dcmp.subdirs.values():
|
||||
handle_diff_etc_files(sub_dcmp)
|
||||
|
||||
handle_diff_etc_files(etc_diff)
|
||||
|
||||
for override, keep_new in overrides_keep_new.items():
|
||||
if (
|
||||
override.startswith("/etc/")
|
||||
and os.path.exists(override)
|
||||
and new_rootfs.exists(override)
|
||||
):
|
||||
subprocess.run(["rm", "-rf", f"{new_rootfs}/{override}"])
|
||||
if keep_new:
|
||||
subprocess.run(
|
||||
[
|
||||
"cp",
|
||||
"-ax",
|
||||
f"{new_rootfs}/usr/{override}",
|
||||
f"{new_rootfs}/{override}",
|
||||
]
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
[
|
||||
"cp",
|
||||
"-ax",
|
||||
override,
|
||||
f"{new_rootfs}/{override}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def merge_var(new_rootfs: RootFS, overrides_keep_new: dict) -> None:
|
||||
"""Merges /var trees.
|
||||
|
||||
Args:
|
||||
new_rootfs: Path to rootfs.
|
||||
overrides_keep_new: Dictionary comprising overrides and whether to keep new.
|
||||
"""
|
||||
subprocess.run(["mv", f"{new_rootfs}/var", f"{new_rootfs}/usr/var"])
|
||||
subprocess.run(["cp", "-ax", "/var", f"{new_rootfs}/var"])
|
||||
|
||||
var_lib_diff = filecmp.dircmp(
|
||||
f"{new_rootfs}/usr/var/lib/", f"{new_rootfs}/var/lib/"
|
||||
)
|
||||
|
||||
dir_name = f"{new_rootfs}/var/lib/"
|
||||
for name in var_lib_diff.left_only:
|
||||
if os.path.isdir(os.path.join(var_lib_diff.left, name)):
|
||||
subprocess.run(
|
||||
["cp", "-ax", os.path.join(var_lib_diff.left, name), dir_name]
|
||||
)
|
||||
|
||||
for override, keep_new in overrides_keep_new.items():
|
||||
if (
|
||||
override.startswith("/var/")
|
||||
and os.path.exists(override)
|
||||
and new_rootfs.exists(override)
|
||||
):
|
||||
subprocess.run(["rm", "-rf", f"{new_rootfs}/{override}"])
|
||||
if keep_new:
|
||||
subprocess.run(
|
||||
[
|
||||
"cp",
|
||||
"-ax",
|
||||
f"{new_rootfs}/usr/{override}",
|
||||
f"{new_rootfs}/{override}",
|
||||
]
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
[
|
||||
"cp",
|
||||
"-ax",
|
||||
override,
|
||||
f"{new_rootfs}/{override}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def replace_boot_files() -> None:
|
||||
"""Replace files in /boot with those from new rootfs."""
|
||||
new_boot_files = []
|
||||
|
||||
for f in os.listdir("/.update_rootfs/boot"):
|
||||
if not os.path.isdir(f"/.update_rootfs/boot/{f}"):
|
||||
subprocess.run(["mv", f"/.update_rootfs/boot/{f}", "/boot"])
|
||||
new_boot_files.append(f)
|
||||
|
||||
for f in os.listdir("/boot"):
|
||||
if not os.path.isdir(f"/boot/{f}"):
|
||||
if f not in new_boot_files:
|
||||
subprocess.run(["rm", "-f", f"/boot/{f}"])
|
||||
|
||||
subprocess.run(["grub-mkconfig", "-o", "/boot/grub/grub.cfg"])
|
||||
|
||||
|
||||
def update() -> None:
|
||||
"""Update system.
|
||||
|
||||
Args:
|
||||
image_name: Name of image to rebase to.
|
||||
"""
|
||||
|
||||
update_cleanup()
|
||||
|
||||
system_config = helpers.get_system_config()
|
||||
|
||||
output.info("generating new rootfs")
|
||||
|
||||
subprocess.run(["rm", "-rf", "/var/cache/akshara"])
|
||||
Path("/var/cache/akshara/rootfs").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
new_rootfs = gen_rootfs(system_config, "/var/cache/akshara/rootfs")
|
||||
|
||||
overrides_keep_new = (
|
||||
{
|
||||
override["path"]: override["keep"] == "new"
|
||||
for override in system_config["override"]
|
||||
}
|
||||
if isinstance(system_config.get("override"), list)
|
||||
else {}
|
||||
)
|
||||
|
||||
merge_etc(new_rootfs, overrides_keep_new)
|
||||
|
||||
try:
|
||||
# Store new_passwd_entries for users.merge_group() call
|
||||
new_passwd_entries = users.merge_passwd(new_rootfs)
|
||||
except Exception:
|
||||
output.error("malformed /etc/passwd")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
users.merge_shadow(new_rootfs)
|
||||
except Exception:
|
||||
output.error("malformed /etc/shadow")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
users.merge_group(new_rootfs, new_passwd_entries)
|
||||
except Exception:
|
||||
output.error("malformed /etc/group")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
users.merge_gshadow(new_rootfs, new_passwd_entries)
|
||||
except Exception:
|
||||
output.error("malformed /etc/shadow")
|
||||
sys.exit(1)
|
||||
|
||||
merge_var(new_rootfs, overrides_keep_new)
|
||||
|
||||
if (
|
||||
len(
|
||||
[
|
||||
kernel
|
||||
for kernel in os.listdir(f"{new_rootfs}/boot")
|
||||
if kernel.startswith("vmlinuz")
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
output.error("new rootfs contains no kernel")
|
||||
output.error("refusing to proceed with applying update")
|
||||
exit(1)
|
||||
|
||||
subprocess.run(["cp", "-ax", str(new_rootfs), "/.update_rootfs"])
|
||||
|
||||
replace_boot_files()
|
||||
|
||||
print()
|
||||
209
usr/lib/akshara/utils/users.py
Normal file
209
usr/lib/akshara/utils/users.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
def merge_passwd(new_rootfs) -> list:
|
||||
"""Merge /etc/passwd from host and new rootfs.
|
||||
|
||||
Args:
|
||||
new_rootfs: Path to new root filesystem.
|
||||
|
||||
Returns:
|
||||
A list of the newly generated passwd entries.
|
||||
"""
|
||||
|
||||
with open("/etc/passwd") as f:
|
||||
current_passwd_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/usr/etc/passwd") as f:
|
||||
current_system_passwd_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open(f"{new_rootfs}/etc/passwd") as f:
|
||||
new_rootfs_passwd_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
new_passwd_entries = list(new_rootfs_passwd_entries.values())
|
||||
|
||||
for user in set(current_passwd_entries.keys()) - set(
|
||||
current_system_passwd_entries.keys()
|
||||
):
|
||||
if (
|
||||
int(current_passwd_entries[user].split(":")[2]) >= 1000
|
||||
and user not in new_rootfs_passwd_entries.keys()
|
||||
):
|
||||
new_passwd_entries.append(current_passwd_entries[user])
|
||||
|
||||
with open("/.new.etc/passwd", "w") as f:
|
||||
for user in new_passwd_entries:
|
||||
f.write(user + "\n")
|
||||
|
||||
return new_passwd_entries
|
||||
|
||||
|
||||
def merge_shadow(new_rootfs):
|
||||
"""Merge /etc/shadow from host and new rootfs.
|
||||
|
||||
Args:
|
||||
new_rootfs: Path to new root filesystem.
|
||||
"""
|
||||
|
||||
with open("/etc/passwd") as f:
|
||||
current_passwd_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/etc/shadow") as f:
|
||||
current_shadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/usr/etc/shadow") as f:
|
||||
current_system_shadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open(f"{new_rootfs}/etc/shadow") as f:
|
||||
new_rootfs_shadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
new_shadow_entries = list(new_rootfs_shadow_entries.values())
|
||||
|
||||
for user in set(current_shadow_entries.keys()) - set(
|
||||
current_system_shadow_entries.keys()
|
||||
):
|
||||
if (
|
||||
int(current_passwd_entries[user].split(":")[2]) >= 1000
|
||||
and user not in new_rootfs_shadow_entries.keys()
|
||||
):
|
||||
new_shadow_entries.append(current_shadow_entries[user])
|
||||
|
||||
with open("/.new.etc/shadow", "w") as f:
|
||||
for user in new_shadow_entries:
|
||||
f.write(user + "\n")
|
||||
|
||||
|
||||
def merge_group(new_rootfs, new_passwd_entries):
|
||||
"""Merge /etc/group from host and new rootfs.
|
||||
|
||||
Args:
|
||||
new_rootfs: Path to new root filesystem.
|
||||
"""
|
||||
|
||||
with open("/etc/group") as f:
|
||||
current_group_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/usr/etc/group") as f:
|
||||
current_system_group_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open(f"{new_rootfs}/etc/group") as f:
|
||||
new_rootfs_group_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
new_group_entries_names = list(
|
||||
set(new_rootfs_group_entries.keys()) - set(current_system_group_entries.keys())
|
||||
)
|
||||
new_group_entries = [
|
||||
new_rootfs_group_entries[new_group_entries_name]
|
||||
for new_group_entries_name in new_group_entries_names
|
||||
]
|
||||
|
||||
for group in (
|
||||
set(current_system_group_entries.keys()) & set(new_rootfs_group_entries.keys())
|
||||
) & set(current_group_entries.keys()):
|
||||
old_group_entry = current_group_entries[group]
|
||||
new_group_entry = new_rootfs_group_entries[group]
|
||||
for member_user in old_group_entry.split(":")[3].split(","):
|
||||
if member_user in [
|
||||
passwd_entry.split(":")[0] for passwd_entry in new_passwd_entries
|
||||
] and member_user not in new_group_entry.split(":")[3].split(","):
|
||||
if new_group_entry.split(":")[3] == "":
|
||||
new_group_entry += member_user
|
||||
else:
|
||||
new_group_entry += "," + member_user
|
||||
new_group_entries.append(new_group_entry)
|
||||
|
||||
for group in set(current_group_entries.keys()) - set(
|
||||
current_system_group_entries.keys()
|
||||
):
|
||||
if (
|
||||
int(current_group_entries[group].split(":")[2]) >= 1000
|
||||
and group not in new_rootfs_group_entries.keys()
|
||||
):
|
||||
new_group_entries.append(current_group_entries[group])
|
||||
|
||||
with open("/.new.etc/group", "w") as f:
|
||||
for group in new_group_entries:
|
||||
f.write(group + "\n")
|
||||
|
||||
|
||||
def merge_gshadow(new_rootfs, new_passwd_entries):
|
||||
"""Merge /etc/gshadow from host and new rootfs.
|
||||
|
||||
Args:
|
||||
new_rootfs: Path to new root filesystem.
|
||||
"""
|
||||
|
||||
with open("/etc/group") as f:
|
||||
current_group_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/etc/gshadow") as f:
|
||||
current_gshadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open("/usr/etc/gshadow") as f:
|
||||
current_system_gshadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
with open(f"{new_rootfs}/etc/gshadow") as f:
|
||||
new_rootfs_gshadow_entries = {
|
||||
line.strip().split(":")[0]: line.strip() for line in f if line.strip()
|
||||
}
|
||||
|
||||
new_gshadow_entries_names = list(
|
||||
set(new_rootfs_gshadow_entries.keys())
|
||||
- set(current_system_gshadow_entries.keys())
|
||||
)
|
||||
new_gshadow_entries = [
|
||||
new_rootfs_gshadow_entries[new_gshadow_entries_name]
|
||||
for new_gshadow_entries_name in new_gshadow_entries_names
|
||||
]
|
||||
|
||||
for group in (
|
||||
set(current_system_gshadow_entries.keys())
|
||||
& set(new_rootfs_gshadow_entries.keys())
|
||||
) & set(current_gshadow_entries.keys()):
|
||||
old_gshadow_entry = current_gshadow_entries[group]
|
||||
new_gshadow_entry = new_rootfs_gshadow_entries[group]
|
||||
for member_user in old_gshadow_entry.split(":")[3].split(","):
|
||||
if member_user in [
|
||||
passwd_entry.split(":")[0] for passwd_entry in new_passwd_entries
|
||||
] and member_user not in new_gshadow_entry.split(":")[3].split(","):
|
||||
if new_gshadow_entry.split(":")[3] == "":
|
||||
new_gshadow_entry += member_user
|
||||
else:
|
||||
new_gshadow_entry += "," + member_user
|
||||
new_gshadow_entries.append(new_gshadow_entry)
|
||||
|
||||
for group in set(current_gshadow_entries.keys()) - set(
|
||||
current_system_gshadow_entries.keys()
|
||||
):
|
||||
if (
|
||||
int(current_group_entries[group].split(":")[2]) >= 1000
|
||||
and group not in new_rootfs_gshadow_entries.keys()
|
||||
):
|
||||
new_gshadow_entries.append(current_gshadow_entries[group])
|
||||
|
||||
with open("/.new.etc/gshadow", "w") as f:
|
||||
for group in new_gshadow_entries:
|
||||
f.write(group + "\n")
|
||||
37
usr/lib/dracut/modules.d/10akshara/handle-update.sh
Executable file
37
usr/lib/dracut/modules.d/10akshara/handle-update.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo
|
||||
|
||||
# Remove "$NEWROOT"/.successful-update if exists
|
||||
rm -f "$NEWROOT"/.successful-update "$NEWROOT"/.update
|
||||
|
||||
# Check if there is an available update
|
||||
if [ -d "$NEWROOT"/.update_rootfs ]; then
|
||||
# Available, rename old /usr and move new /usr to /
|
||||
if [ -d "$NEWROOT"/.update_rootfs/usr ]; then
|
||||
rm -rf "$NEWROOT"/.old.usr
|
||||
mv "$NEWROOT"/usr "$NEWROOT"/.old.usr >/dev/null 2>&1
|
||||
mv "$NEWROOT"/.update_rootfs/usr "$NEWROOT"/usr
|
||||
fi
|
||||
|
||||
# Same for /etc
|
||||
if [ -d "$NEWROOT"/.update_rootfs/etc ]; then
|
||||
rm -rf "$NEWROOT"/.old.etc
|
||||
mv "$NEWROOT"/etc "$NEWROOT"/.old.etc >/dev/null 2>&1
|
||||
mv "$NEWROOT"/.update_rootfs/etc "$NEWROOT"/etc
|
||||
fi
|
||||
|
||||
# Same for /var
|
||||
if [ -d "$NEWROOT"/.update_rootfs/var ]; then
|
||||
rm -rf "$NEWROOT"/.old.var
|
||||
mv "$NEWROOT"/var "$NEWROOT"/.old.var >/dev/null 2>&1
|
||||
mv "$NEWROOT"/.update_rootfs/var "$NEWROOT"/var
|
||||
fi
|
||||
|
||||
rm -rf "$NEWROOT"/.old.update_rootfs
|
||||
mv "$NEWROOT"/.update_rootfs "$NEWROOT"/.old.update_rootfs
|
||||
touch "$NEWROOT"/.successful-update
|
||||
fi
|
||||
|
||||
mkdir -p "$NEWROOT"/.blendOS-overlays/usr
|
||||
mount -t overlay overlay -o index=off -o metacopy=off -o ro,lowerdir="$NEWROOT"/usr,upperdir="$NEWROOT"/.blendOS-overlays/usr "$NEWROOT"/usr
|
||||
20
usr/lib/dracut/modules.d/10akshara/module-setup.sh
Executable file
20
usr/lib/dracut/modules.d/10akshara/module-setup.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
check() {
|
||||
return 0
|
||||
}
|
||||
|
||||
depends() {
|
||||
echo base
|
||||
return 0
|
||||
}
|
||||
|
||||
installkernel() {
|
||||
hostonly="" instmods overlay
|
||||
}
|
||||
|
||||
install() {
|
||||
inst touch
|
||||
|
||||
inst_hook pre-pivot 15 "$moddir/handle-update.sh"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue