feat: initial commit
This commit is contained in:
commit
8085c747cb
14 changed files with 926 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
||||||
1
usr/bin/akshara
Symbolic link
1
usr/bin/akshara
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../lib/akshara/akshara
|
||||||
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