diff --git a/.gitignore b/.gitignore index 3e3a80b..ab34496 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ build-javascript +cache/ diff --git a/alpine.ts b/alpine.ts new file mode 100644 index 0000000..5dbcd92 --- /dev/null +++ b/alpine.ts @@ -0,0 +1,134 @@ +import { + chmod, + copyFile, + mkdir, + mkdtemp, + opendir, + rm, + rmdir, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { cwd } from "node:process"; +import { execFile } from "./helpers.js"; + +export class Alpine { + dir: string; + private constructor({ dir }: { dir: string }) { + this.dir = dir; + } + + async mkdir(dir: string, opts?: { recursive: boolean }): Promise { + await mkdir(path.join(this.dir, dir), opts); + } + async writeFile(filePath: string, content: string): Promise { + await this.mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(path.join(this.dir, filePath), content); + } + async writeExecutable(filePath: string, content: string): Promise { + await this.writeFile(filePath, content); + await chmod(path.join(this.dir, filePath), 700); + } + async sudoWriteExecutable(filePath: string, content: string): Promise { + const dir = await mkdtemp( + path.join(tmpdir(), "define-alpine-sudoWriteExecutable-") + ); + try { + const tmpFile = path.join(dir, path.basename(filePath)); + const finalPath = path.join(this.dir, filePath); + await writeFile(tmpFile, content); + await execFile("sudo", [ + "mkdir", + "--parents", + path.join(this.dir, path.dirname(filePath)), + ]); + await execFile("sudo", ["mv", tmpFile, finalPath]); + await execFile("sudo", ["chmod", "700", finalPath]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + } + private getRelativeSymlink( + target: string, + filePath: string + ): { target: string; filePath: string } { + const realFilePath = path.join(this.dir, filePath); + return { + target: path.relative( + path.dirname(realFilePath), + path.join(this.dir, target) + ), + filePath: realFilePath, + }; + } + async symlink(_target: string, _filePath: string): Promise { + const { target, filePath } = this.getRelativeSymlink(_target, _filePath); + await symlink(target, filePath); + } + async sudoSymlink(_target: string, _filePath: string): Promise { + const { target, filePath } = this.getRelativeSymlink(_target, _filePath); + await execFile("sudo", ["ln", "-s", target, filePath]); + } + + async addPackages(packages: string[]): Promise { + await execFile("sudo", [ + "apk", + "add", + "--clean-protected", + "--root", + this.dir, + ...packages, + ]); + } + + static async makeWorld({ + dir, + packages, + }: { + dir: string; + packages?: string[]; + }): Promise { + const apkDir = path.join(dir, "/etc/apk"); + await mkdir(apkDir, { recursive: true }); + + // hack + { + const cacheDir = path.join(cwd(), "cache"); + await mkdir("cache", { recursive: true }); + await symlink(cacheDir, path.join(apkDir, "cache")); + } + + { + const apkKeysDir = path.join(apkDir, "keys"); + const keysSrcDir = "alpine/keys"; + await mkdir(apkKeysDir); + for await (const { name } of await opendir(keysSrcDir)) + await copyFile( + path.join(keysSrcDir, name), + path.join(apkKeysDir, name) + ); + } + + await writeFile( + path.join(apkDir, "repositories"), + [ + "https://dl-cdn.alpinelinux.org/alpine/v3.17/main", + "https://dl-cdn.alpinelinux.org/alpine/v3.17/community", + ].join("\n") + ); + await execFile("sudo", [ + "apk", + "add", + "--initdb", + "--clean-protected", + "--root", + dir, + ...["alpine-baselayout", "busybox", "libc-utils", "alpine-keys"], + ...(packages || []), + ]); + + return new Alpine({ dir }); + } +} diff --git a/helpers.ts b/helpers.ts new file mode 100644 index 0000000..8276089 --- /dev/null +++ b/helpers.ts @@ -0,0 +1,8 @@ +import { promisify } from "node:util"; +import { + execFile as execFileCallback, + spawn as spawnCallback, +} from "node:child_process"; + +export const execFile = promisify(execFileCallback); +export const spawn = promisify(spawnCallback); diff --git a/index.ts b/index.ts index 6fcbea4..09d61a6 100644 --- a/index.ts +++ b/index.ts @@ -1,49 +1,50 @@ -import { - execFile as execFileCallback, - spawn as spawnCallback, -} from "node:child_process"; -import { copyFile, mkdir, mkdtemp, opendir, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { promisify } from "node:util"; -const execFile = promisify(execFileCallback); -const spawn = promisify(spawnCallback); +import { Alpine } from "./alpine.js"; +import { spawn } from "./helpers.js"; +import { Runit } from "./runit/index.js"; { const rootfsDir = await mkdtemp(path.join(tmpdir(), "define-alpine-")); console.debug(rootfsDir); - await makeWorld(rootfsDir); + const alpine = await Alpine.makeWorld({ dir: rootfsDir }); + const runit = await Runit.setup(alpine); + // await makeService({ + // parentDir: rootfsDir, + // name: "grafana", + // packages: ["grafana"], + // setup: async (dir) => {}, + // initScript: async (dir) => {}, + // }); try { await spawn("sudo", ["chroot", rootfsDir], { stdio: "inherit" }); } catch {} } -async function makeWorld(dir: string): Promise { - const apkDir = path.join(dir, "/etc/apk"); - await mkdir(apkDir, { recursive: true }); +// interface Service {} +// async function makeService({ +// parentDir, +// name, +// packages, +// setup, +// initScript: _initScript, +// }: { +// parentDir: string; +// name: string; +// packages?: string[]; +// setup: (dir: string) => Promise; +// initScript: (dir: string) => Promise; +// }) { +// const rootsDir = path.join(parentDir, "/nulo/roots/"); +// await mkdir(rootsDir, { recursive: true }); - { - const apkKeysDir = path.join(apkDir, "keys"); - const keysSrcDir = "alpine/keys"; - await mkdir(apkKeysDir); - for await (const { name } of await opendir(keysSrcDir)) - await copyFile(path.join(keysSrcDir, name), path.join(apkKeysDir, name)); - } +// const alpine = await Alpine.makeWorld({ +// dir: path.join(rootsDir, name), +// packages, +// }); - await writeFile( - path.join(apkDir, "repositories"), - [ - "https://dl-cdn.alpinelinux.org/alpine/v3.17/main", - "https://dl-cdn.alpinelinux.org/alpine/v3.17/community", - ].join("\n") - ); - await execFile("sudo", [ - "apk", - "add", - "--initdb", - "--clean-protected", - "--root", - dir, - ...["alpine-baselayout", "busybox", "libc-utils", "alpine-keys"], - ]); -} +// await setup(alpine.dir); + +// // const initScript = await _initScript(rootfsDir); +// } diff --git a/package.json b/package.json index 94fdbf5..4f8d76f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "", "main": "index.js", "scripts": { - "run": "esbuild --target=node18 --sourcemap --outdir=build-javascript --outbase=. *.ts && node --enable-source-maps build-javascript/index.js" + "run": "esbuild --target=node18 --sourcemap --outdir=build-javascript --outbase=. *.ts **/*.ts && node --enable-source-maps build-javascript/index.js" }, "keywords": [], "author": "", diff --git a/runit/index.ts b/runit/index.ts new file mode 100644 index 0000000..ed4cc75 --- /dev/null +++ b/runit/index.ts @@ -0,0 +1,123 @@ +import { readFile } from "fs/promises"; +import path from "path"; +import { Alpine } from "../alpine.js"; + +export class Runit { + alpine: Alpine; + private constructor({ alpine }: { alpine: Alpine }) { + this.alpine = alpine; + } + + private static async getScript(name: string): Promise { + return await readFile(path.join("runit/scripts", name), "utf8"); + } + + static async setup(alpine: Alpine): Promise { + await alpine.mkdir("/etc/runit/runsvdir/default", { recursive: true }); + await alpine.symlink( + "/etc/runit/runsvdir/default", + "/etc/runit/runsvdir/current" + ); + await alpine.symlink("/etc/runit/runsvdir/current", "/etc/service"); + await alpine.symlink("/run/runit/stopit", "/etc/runit/stopit"); + await alpine.symlink("/run/runit/reboot", "/etc/runit/reboot"); + + // Estos scripts fueron robados de Void Linux + await alpine.writeExecutable( + "/etc/runit/functions", + await this.getScript("functions") + ); + + await alpine.writeExecutable( + "/etc/runit/core-services/00-pseudofs.sh", + await this.getScript("00-pseudofs.sh") + ); + await alpine.writeExecutable( + "/etc/runit/core-services/01-static-devnodes.sh", + await this.getScript("01-static-devnodes.sh") + ); + await alpine.writeExecutable( + "/etc/runit/core-services/02-udev.sh", + await this.getScript("02-udev.sh") + ); + await alpine.writeExecutable( + "/etc/runit/core-services/03-filesystems.sh", + await this.getScript("03-filesystems.sh") + ); + await alpine.writeExecutable( + "/etc/runit/core-services/04-swap.sh", + await this.getScript("04-swap.sh") + ); + await alpine.writeExecutable( + "/etc/runit/core-services/05-misc.sh", + await this.getScript("05-misc.sh") + ); + + // https://wiki.gentoo.org/wiki/Runit#Reboot_and_shutdown + await alpine.sudoWriteExecutable( + "/usr/local/sbin/rpoweroff", + `#!/bin/sh +runit-init 0` + ); + await alpine.sudoWriteExecutable( + "/usr/local/sbin/rreboot", + `#!/bin/sh +runit-init 6` + ); + + await alpine.addPackages(["runit", "eudev"]); + + const runit = new Runit({ alpine }); + + await runit.addService( + "getty-tty1", + `#!/bin/sh +exec chpst -P getty 38400 tty1 linux`, + false + ); + await runit.addService( + "getty-tty2", + `#!/bin/sh +exec chpst -P getty 38400 tty2 linux`, + false + ); + await runit.addService( + "getty-ttyS0", + `#!/bin/sh +exec chpst -P getty 38400 ttyS0 linux`, + false + ); + + return runit; + } + + async addService( + name: string, + script: string, + log: boolean = true + ): Promise { + const runScriptPath = path.join("/etc/sv/", name, "/run"); + await this.alpine.sudoWriteExecutable(runScriptPath, script); + if (log) { + const logScriptPath = path.join("/etc/sv/", name, "/log/run"); + await this.alpine.sudoWriteExecutable( + logScriptPath, + `#!/bin/sh +exec logger -p daemon.info -t '${name}'` + ); + await this.alpine.symlink( + `/run/runit/supervise.${name}.log`, + path.join("/etc/sv/", name, "/log/supervise") + ); + } + // Activar servicio + await this.alpine.sudoSymlink( + path.join("/etc/sv/", name), + path.join("/etc/runit/runsvdir/default/", name) + ); + await this.alpine.sudoSymlink( + `/run/runit/supervise.${name}`, + path.join("/etc/sv/", name, "/supervise") + ); + } +} diff --git a/runit/scripts/00-pseudofs.sh b/runit/scripts/00-pseudofs.sh new file mode 100644 index 0000000..215d0a8 --- /dev/null +++ b/runit/scripts/00-pseudofs.sh @@ -0,0 +1,48 @@ +msg "Mounting pseudo-filesystems..." +mountpoint -q /proc || mount -o nosuid,noexec,nodev -t proc proc /proc +mountpoint -q /sys || mount -o nosuid,noexec,nodev -t sysfs sys /sys +mountpoint -q /run || mount -o mode=0755,nosuid,nodev -t tmpfs run /run +mountpoint -q /dev || mount -o mode=0755,nosuid -t devtmpfs dev /dev +mkdir -p -m0755 /run/runit /run/lvm /run/user /run/lock /run/log /dev/pts /dev/shm +mountpoint -q /dev/pts || mount -o mode=0620,gid=5,nosuid,noexec -n -t devpts devpts /dev/pts +mountpoint -q /dev/shm || mount -o mode=1777,nosuid,nodev -n -t tmpfs shm /dev/shm +mountpoint -q /sys/kernel/security || mount -n -t securityfs securityfs /sys/kernel/security + +if [ -d /sys/firmware/efi/efivars ]; then + mountpoint -q /sys/firmware/efi/efivars || mount -o nosuid,noexec,nodev -t efivarfs efivarfs /sys/firmware/efi/efivars +fi + +if [ -z "$VIRTUALIZATION" ]; then + _cgroupv1="" + _cgroupv2="" + + case "${CGROUP_MODE:-hybrid}" in + legacy) + _cgroupv1="/sys/fs/cgroup" + ;; + hybrid) + _cgroupv1="/sys/fs/cgroup" + _cgroupv2="${_cgroupv1}/unified" + ;; + unified) + _cgroupv2="/sys/fs/cgroup" + ;; + esac + + # cgroup v1 + if [ -n "$_cgroupv1" ]; then + mountpoint -q "$_cgroupv1" || mount -o mode=0755 -t tmpfs cgroup "$_cgroupv1" + while read -r _subsys_name _hierarchy _num_cgroups _enabled; do + [ "$_enabled" = "1" ] || continue + _controller="${_cgroupv1}/${_subsys_name}" + mkdir -p "$_controller" + mountpoint -q "$_controller" || mount -t cgroup -o "$_subsys_name" cgroup "$_controller" + done < /proc/cgroups + fi + + # cgroup v2 + if [ -n "$_cgroupv2" ]; then + mkdir -p "$_cgroupv2" + mountpoint -q "$_cgroupv2" || mount -t cgroup2 -o nsdelegate cgroup2 "$_cgroupv2" + fi +fi \ No newline at end of file diff --git a/runit/scripts/01-static-devnodes.sh b/runit/scripts/01-static-devnodes.sh new file mode 100644 index 0000000..2bf15c8 --- /dev/null +++ b/runit/scripts/01-static-devnodes.sh @@ -0,0 +1,6 @@ +# Some kernel modules must be loaded before starting udev(7). +# Load them by looking at the output of `kmod static-nodes`. + +for f in $(kmod static-nodes 2>/dev/null|awk '/Module/ {print $2}'); do + modprobe -bq $f 2>/dev/null +done diff --git a/runit/scripts/02-udev.sh b/runit/scripts/02-udev.sh new file mode 100644 index 0000000..18bfa1e --- /dev/null +++ b/runit/scripts/02-udev.sh @@ -0,0 +1,15 @@ +[ -n "$VIRTUALIZATION" ] && return 0 + +if [ -x /sbin/udevd -o -x /bin/udevd ]; then + _udevd=udevd +else + msg_warn "cannot find udevd!" +fi + +if [ -n "${_udevd}" ]; then + msg "Starting udev and waiting for devices to settle..." + ${_udevd} --daemon + udevadm trigger --action=add --type=subsystems + udevadm trigger --action=add --type=devices + udevadm settle +fi diff --git a/runit/scripts/03-filesystems.sh b/runit/scripts/03-filesystems.sh new file mode 100644 index 0000000..8061b5d --- /dev/null +++ b/runit/scripts/03-filesystems.sh @@ -0,0 +1,78 @@ +[ -n "$VIRTUALIZATION" ] && return 0 + +#msg "Remounting rootfs read-only..." +#mount -o remount,ro / || emergency_shell + +if [ -x /sbin/dmraid -o -x /bin/dmraid ]; then + msg "Activating dmraid devices..." + dmraid -i -ay +fi + +if [ -x /bin/mdadm ]; then + msg "Activating software RAID arrays..." + mdadm -As +fi + +if [ -x /bin/btrfs ]; then + msg "Activating btrfs devices..." + btrfs device scan || emergency_shell +fi + +if [ -x /sbin/vgchange -o -x /bin/vgchange ]; then + msg "Activating LVM devices..." + vgchange --sysinit -a ay || emergency_shell +fi + +if [ -e /etc/crypttab ]; then + msg "Activating encrypted devices..." + awk -f /etc/runit/crypt.awk /etc/crypttab + + if [ -x /sbin/vgchange -o -x /bin/vgchange ]; then + msg "Activating LVM devices for dm-crypt..." + vgchange --sysinit -a ay || emergency_shell + fi +fi + +if [ -x /usr/bin/zpool -a -x /usr/bin/zfs ]; then + if [ -e /etc/zfs/zpool.cache ]; then + msg "Importing cached ZFS pools..." + zpool import -N -a -c /etc/zfs/zpool.cache + else + msg "Scanning for and importing ZFS pools..." + zpool import -N -a -o cachefile=none + fi + + msg "Mounting ZFS file systems..." + zfs mount -a -l + + msg "Sharing ZFS file systems..." + zfs share -a + + # NOTE(dh): ZFS has ZVOLs, block devices on top of storage pools. + # In theory, it would be possible to use these as devices in + # dmraid, btrfs, LVM and so on. In practice it's unlikely that + # anybody is doing that, so we aren't supporting it for now. +fi + +[ -f /fastboot ] && FASTBOOT=1 +[ -f /forcefsck ] && FORCEFSCK="-f" +for arg in $(cat /proc/cmdline); do + case $arg in + fastboot) FASTBOOT=1;; + forcefsck) FORCEFSCK="-f";; + esac +done + +if [ -z "$FASTBOOT" ]; then + msg "Checking filesystems:" + fsck -A -T -a -t noopts=_netdev $FORCEFSCK + if [ $? -gt 1 ]; then +s emergency_shell + fi +fi + +msg "Mounting all non-network filesystems..." +mount -a -t "nosysfs,nonfs,nonfs4,nosmbfs,nocifs" -O no_netdev || emergency_shell +# data module +msg "Creating and mounting data directories..." +/usr/local/bin/mount-data || emergency_shell diff --git a/runit/scripts/04-swap.sh b/runit/scripts/04-swap.sh new file mode 100644 index 0000000..f36bc1a --- /dev/null +++ b/runit/scripts/04-swap.sh @@ -0,0 +1,4 @@ +[ -n "$VIRTUALIZATION" ] && return 0 + +msg "Initializing swap..." +swapon -a || emergency_shell diff --git a/runit/scripts/05-misc.sh b/runit/scripts/05-misc.sh new file mode 100644 index 0000000..258e034 --- /dev/null +++ b/runit/scripts/05-misc.sh @@ -0,0 +1,8 @@ +install -m0664 -o root -g utmp /dev/null /run/utmp +#halt -B # for wtmp + +msg "Setting up loopback interface..." +ip link set up dev lo + +msg "Setting hostname..." +hostname -F /etc/hostname diff --git a/runit/scripts/functions b/runit/scripts/functions new file mode 100644 index 0000000..5232e68 --- /dev/null +++ b/runit/scripts/functions @@ -0,0 +1,52 @@ +msg() { + # bold + printf "\\033[1m=> $@\\033[m\\n" +} + +msg_ok() { + # bold/green + printf "\\033[1m\\033[32m OK\\033[m\\n" +} + +msg_error() { + # bold/red + printf "\\033[1m\\033[31mERROR: $@\\033[m\\n" +} + +msg_warn() { + # bold/yellow + printf "\\033[1m\\033[33mWARNING: $@\\033[m\\n" +} + +emergency_shell() { + echo + echo "Cannot continue due to errors above, starting emergency shell." + echo "When ready type exit to continue booting." + /bin/sh -l +} + +detect_virt() { + # Detect LXC (and other) containers + [ -z "${container+x}" ] || export VIRTUALIZATION=1 +} + +deactivate_vgs() { + _group=${1:-All} + if [ -x /sbin/vgchange -o -x /bin/vgchange ]; then + vgs=$(vgs|wc -l) + if [ $vgs -gt 0 ]; then + msg "Deactivating $_group LVM Volume Groups..." + vgchange -an + fi + fi +} + +deactivate_crypt() { + if [ -x /sbin/dmsetup -o -x /bin/dmsetup ]; then + msg "Deactivating Crypt Volumes" + for v in $(dmsetup ls --target crypt --exec "dmsetup info -c --noheadings -o open,name"); do + [ ${v%%:*} = "0" ] && cryptsetup close ${v##*:} + done + deactivate_vgs "Crypt" + fi +}