This commit is contained in:
Nocturn9x 2022-03-16 14:36:21 +01:00
commit ea1f3cc6f9
9 changed files with 230 additions and 49 deletions

3
.gitignore vendored
View File

@ -5,7 +5,6 @@ main
*.iso *.iso
nimd nimd
main main
boot.sh
rootfs rootfs
packervm packervm
test test
@ -14,7 +13,5 @@ initrd*
vmlinuz-linux vmlinuz-linux
vmlinuz* vmlinuz*
debian* debian*
start.sh
rebuild.sh
vm.qcow2 vm.qcow2
*.tar.* *.tar.*

View File

@ -116,4 +116,10 @@ workers = 1 # Number of worker processes to use
restartDelay = 10 # Delay (seconds) that nimd will wait before restarting itself after crashes restartDelay = 10 # Delay (seconds) that nimd will wait before restarting itself after crashes
sigtermDelay = 90 # Delay (seconds) that nimd will wait before terminating child processes with sigtermDelay = 90 # Delay (seconds) that nimd will wait before terminating child processes with
# SIGKILL after sending a more gentle SIGTERM upon shutdown or exit # SIGKILL after sending a more gentle SIGTERM upon shutdown or exit
``` ```
## Testing NimD
NimD is not quite ready for production yet, but in the `scripts` folder you can find a few simple bash scripts to test NimD
in a minimal Alpine Linux VM using QEMU. Note that due to weirdness in how stdout is handled on the VGA port, the VM will use
the serial port (ttyS0) as output by default (you can change this in the kernel parameters)

View File

@ -7,7 +7,7 @@ restartDelay = 10
sigtermDelay = 90 sigtermDelay = 90
[Logging] [Logging]
level = info level = debug
logFile = /var/log/nimd logFile = /var/log/nimd
[Filesystem] [Filesystem]

115
scripts/boot.sh Executable file
View File

@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
usage() {
cat << EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-m] [-b] -k kernel -i initrd -r rootfs
Alpinemini vm
Available options:
-h, --help Print this help and exit
-v, --verbose Print script debug info
-k, --kernel Specify kernel file
-i, --initrd Specify initrd file
-m, --memory Set maximum vm memory
-b, --build Build a new disk (and then boot)
EOF
exit
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
die() {
local msg=$1
local code=${2-1}
msg "$msg"
exit "$code"
}
parse_params() {
build=0
rootfs=''
kernel=''
initrd=''
memory=''
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-b | --build) build=1 ;; # build disk
-k | --kernel) # kernel file
kernel="${2-}"
shift
;;
-i | --initrd) # initrd file
initrd="${2-}"
shift
;;
-m | --memory) # max memory
memory="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
[[ -z "${kernel-}" ]] && die "${RED}Missing required parameter:${NOFORMAT} kernel"
[[ -z "${initrd-}" ]] && die "${RED}Missing required parameter:${NOFORMAT} initrd"
return 0
}
setup_colors
parse_params "$@"
if ! [ -x "$(command -v qemu-system-x86_64)" ]; then
echo '${RED}Error:${NOFORMAT} unable to find qemu-system-x86_64, please install it first.' >&2
exit 1
fi
if ! [ -x "$(command -v qemu-img)" ]; then
echo '${RED}Error:${NOFORMAT} unable to find qemu-img, please install it first.' >&2
exit 1
fi
if [[ $build -eq 1 ]]
then
msg "${CYAN}Building disk... Please wait${NOFORMAT}"
qemu-img create -f qcow2 vm.qcow2 800M
qemu-system-x86_64 -m 256M -smp 1 -drive file=packervm/packer.qcow2,if=virtio,readonly=on -drive file=vm.qcow2,if=virtio -enable-kvm -fsdev local,id=rootfs_dev,path=rootfs,security_model=none -device virtio-9p-pci,fsdev=rootfs_dev,mount_tag=rootfs -display none
fi
qemumem=''
if ! [[ $memory -eq "" ]]
then
qemumem="-m ${memory}"
fi
qemu-system-x86_64 -net nic,model=virtio,netdev=user.0 -netdev user,id=user.0 -drive file=vm.qcow2,if=virtio -enable-kvm -kernel ${kernel} -initrd ${initrd} ${qemumem} -append 'root=/dev/vda1 rw quiet modules=ext4 console=ttyS0 init=/bin/nimd'

10
scripts/rebuild.sh Executable file
View File

@ -0,0 +1,10 @@
# Build the environment
mkdir -p rootfs/etc/nimd
cp nimd.conf rootfs/etc/nimd/nimd.conf
nim -o:rootfs/bin/nimd -d:release --gc:orc --opt:size --passL:"-static" compile src/main.nim
nim -o:rootfs/bin/halt -d:release --gc:orc --opt:size --passL:"-static" compile src/programs/halt.nim
nim -o:rootfs/bin/reboot -d:release --gc:orc --opt:size --passL:"-static" compile src/programs/reboot.nim
nim -o:rootfs/bin/poweroff -d:release --gc:orc --opt:size --passL:"-static" compile src/programs/poweroff.nim
# Start the VM
./scripts/boot.sh --kernel vmlinuz-linux --initrd initrd-linux.img --memory 1G --build

1
scripts/start.sh Executable file
View File

@ -0,0 +1 @@
./scripts/boot.sh --kernel vmlinuz-linux --initrd initrd-linux.img --memory 1G

View File

@ -28,8 +28,8 @@ proc mainLoop*(logger: Logger, config: NimDConfig, startServices: bool = true) =
if startServices: if startServices:
logger.info("Processing default runlevel") logger.info("Processing default runlevel")
startServices(logger, workers=config.workers, level=Default) startServices(logger, workers=config.workers, level=Default)
logger.debug(&"Unblocking signals") logger.debug(&"Unblocking signals")
unblockSignals(logger) unblockSignals(logger)
logger.info("System initialization complete, idling on control socket") logger.info("System initialization complete, idling on control socket")
var opType: string var opType: string
try: try:
@ -40,40 +40,49 @@ proc mainLoop*(logger: Logger, config: NimDConfig, startServices: bool = true) =
logger.switchToFile() logger.switchToFile()
logger.debug("Entering accept() loop") logger.debug("Entering accept() loop")
while true: while true:
serverSocket.accept(clientSocket) try:
logger.debug(&"Received connection on control socket") serverSocket.accept(clientSocket)
if clientSocket.recv(opType, size=1) == 0: logger.debug(&"Received connection on control socket")
logger.debug(&"Client has disconnected, waiting for new connections") if clientSocket.recv(opType, size=1) == 0:
continue logger.debug(&"Client has disconnected, waiting for new connections")
logger.debug(&"Received operation type '{opType}' via control socket") continue
# The operation type is a single byte: logger.debug(&"Received operation type '{opType}' via control socket")
# - 'p' -> poweroff # The operation type is a single byte:
# - 'r' -> reboot # - 'p' -> poweroff
# - 'h' -> halt # - 'r' -> reboot
# - 's' -> Services-related operations (start, stop, get status, etc.) # - 'h' -> halt
# - 'l' -> Reload in-memory configuration # - 's' -> Services-related operations (start, stop, get status, etc.)
case opType: # - 'l' -> Reload in-memory configuration
of "p": # - 'c' -> Check NimD status (returns "1" if up)
logger.info("Received shutdown request") case opType:
shutdown(logger) of "p":
of "r": logger.info("Received shutdown request")
logger.info("Received reboot request") shutdown(logger)
reboot(logger) of "r":
of "h": logger.info("Received reboot request")
logger.info("Received halt request") reboot(logger)
halt(logger) of "h":
of "s": logger.info("Received halt request")
logger.info("Received service request") halt(logger)
# TODO: Operate on services of "s":
of "l": logger.info("Received service-related request")
logger.info("Received reload request") # TODO: Operate on services
mainLoop(logger, parseConfig(logger, "/etc/nimd/nimd.conf"), startServices=false) of "l":
else: logger.info("Received reload request")
logger.warning(&"Received unknown operation type '{opType}' via control socket, ignoring it") mainLoop(logger, parseConfig(logger, "/etc/nimd/nimd.conf"), startServices=false)
discard of "c":
clientSocket.close() logger.info("Received check request, responding")
clientSocket.send("1")
else:
logger.warning(&"Received unknown operation type '{opType}' via control socket, ignoring it")
discard
except:
logger.error(&"An error occurred while idling on control socket: {getCurrentExceptionMsg()}")
finally:
clientSocket.close()
clientSocket = newSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
except: except:
logger.critical(&"A critical error has occurred while running, restarting the mainloop in {config.restartDelay} seconds! Error -> {getCurrentExceptionMsg()}") logger.critical(&"A critical error has occurred while running, restarting the main loop in {config.restartDelay} seconds! Error -> {getCurrentExceptionMsg()}")
sleepSeconds(config.restartDelay) sleepSeconds(config.restartDelay)
# We *absolutely* cannot die # We *absolutely* cannot die
mainLoop(logger, config, startServices=false) mainLoop(logger, config, startServices=false)

View File

@ -32,16 +32,14 @@ type ShutdownHandler* = ref object
const reboot_codes = {"poweroff": 0x4321fedc'i64, "reboot": 0x01234567'i64, "halt": 0xcdef0123}.toTable() const reboot_codes = {"poweroff": 0x4321fedc'i64, "reboot": 0x01234567'i64, "halt": 0xcdef0123}.toTable()
var shutdownHandlers: seq[ShutdownHandler] = @[]
var sigTermDelay: float = 90
proc newShutdownHandler*(body: proc (logger: Logger, code: int)): ShutdownHandler = proc newShutdownHandler*(body: proc (logger: Logger, code: int)): ShutdownHandler =
result = ShutdownHandler(body: body) result = ShutdownHandler(body: body)
var shutdownHandlers: seq[ShutdownHandler] = @[]
var sigTermDelay: float = 90
proc setSigTermDelay*(delay: int = 90) = proc setSigTermDelay*(delay: int = 90) =
# Sets the sigtermDelay variable # Sets the sigtermDelay variable
sigTermDelay = float(delay) sigTermDelay = float(delay)
@ -97,8 +95,8 @@ proc nimDExit*(logger: Logger, code: int, emerg: bool = true) =
# We're in emergency mode: do not crash the kernel, spawn a shell and exit # We're in emergency mode: do not crash the kernel, spawn a shell and exit
logger.fatal("NimD has entered emergency mode and cannot continue. You will be now (hopefully) dropped in a root shell: you're on your own. May the force be with you") logger.fatal("NimD has entered emergency mode and cannot continue. You will be now (hopefully) dropped in a root shell: you're on your own. May the force be with you")
logger.info("Terminating child processes with SIGKILL") logger.info("Terminating child processes with SIGKILL")
discard execCmd(os.getEnv("SHELL", "/bin/sh")) # TODO: Is this fine? maybe use execProcess
discard posix.kill(-1, SIGKILL) discard posix.kill(-1, SIGKILL)
discard execCmd(os.getEnv("SHELL", "/bin/sh")) # TODO: Is this fine? maybe use execProcess
quit(-1) quit(-1)
logger.warning("The system is shutting down") logger.warning("The system is shutting down")
logger.info("Processing shutdown runlevel") logger.info("Processing shutdown runlevel")

View File

@ -14,6 +14,7 @@
import parseopt import parseopt
import strformat import strformat
import posix import posix
import net
import os import os
# NimD's own stuff # NimD's own stuff
@ -58,9 +59,9 @@ proc addStuff =
depends=(@[newDependency(Other, errorer)]), provides=(@[]), depends=(@[newDependency(Other, errorer)]), provides=(@[]),
stdin="/dev/null", stderr="", stdout="") stdin="/dev/null", stderr="", stdout="")
var shell = newService(name="login", description="A simple login shell", kind=Simple, var shell = newService(name="login", description="A simple login shell", kind=Simple,
workDir=getCurrentDir(), runlevel=Boot, exec="/bin/login -f root", workDir=getCurrentDir(), runlevel=Default, exec="/bin/login -f root",
supervised=true, restart=Always, restartDelay=0, depends=(@[]), provides=(@[]), supervised=true, restart=Always, restartDelay=0, depends=(@[]), provides=(@[]),
useParentStreams=true, stdin="/dev/null", stderr="", stdout="" useParentStreams=true, stdin="/dev/null", stderr="/proc/self/fd/2", stdout="/proc/self/fd/1"
) )
addService(errorer) addService(errorer)
addService(echoer) addService(echoer)
@ -68,10 +69,55 @@ proc addStuff =
addService(shell) addService(shell)
proc checkControlSocket(logger: Logger, config: NimDConfig): bool =
## Performs some startup checks on nim's control
## socket
result = true
var stat_result: Stat
if posix.stat(cstring(config.sock), stat_result) == -1 and posix.errno != 2:
logger.warning(&"Could not stat() {config.sock}, assuming NimD instance isn't running")
elif posix.errno == 2:
logger.debug(&"Control socket path is clear, starting up")
posix.errno = 0
# 2 is ENOENT, which means the file does not exist
# I stole this from /usr/lib/python3.10/stat.py
elif (int(stat_result.st_mode) and 0o170000) != 0o140000:
logger.fatal(&"{config.sock} exists and is not a socket")
result = false
elif dirExists(config.sock):
logger.info("Control socket path is a directory, appending nimd.sock to it")
config.sock = config.sock.joinPath("nimd.sock")
else:
logger.debug("Trying to reach current NimD instance")
var sock = newSocket(Domain.AF_UNIX, SockType.SOCK_STREAM, Protocol.IPPROTO_IP)
try:
sock.connectUnix(config.sock)
logger.info("Control socket already exists, trying to reach current NimD instance")
except OSError:
logger.warning(&"Could not connect to control socket at {config.sock} ({getCurrentExceptionMsg()}), assuming NimD instance isn't running")
try:
removeFile(config.sock)
except OSError:
logger.warning(&"Could not delete dangling control socket at {config.sock} ({getCurrentExceptionMsg()})")
if sock.trySend("c"):
try:
if sock.recv(1, timeout=5) == "1":
logger.error("Another NimD instance is running! Exiting")
result = false
except OSError:
logger.warning(&"Could not read from control socket at {config.sock} ({getCurrentExceptionMsg()}), assuming NimD instance isn't running")
except TimeoutError:
logger.warning(&"Could not read from control socket at {config.sock} ({getCurrentExceptionMsg()}), assuming NimD instance isn't running")
else:
logger.fatal(&"Could not write on control socket at {config.sock}")
result = false
proc main(logger: Logger, config: NimDConfig) = proc main(logger: Logger, config: NimDConfig) =
## NimD's entry point and setup ## NimD's entry point and setup
## function ## function
if not checkControlSocket(logger, config):
return
logger.debug(&"Setting log file to '{config.logFile}'") logger.debug(&"Setting log file to '{config.logFile}'")
setLogFile(file=config.logFile) setLogFile(file=config.logFile)
# Black Lives Matter. This is sarcasm btw. Fuck the left # Black Lives Matter. This is sarcasm btw. Fuck the left
@ -175,8 +221,7 @@ when isMainModule:
quit(EINVAL) # EINVAL - Invalid argument quit(EINVAL) # EINVAL - Invalid argument
else: else:
echo "Usage: nimd [options]" echo "Usage: nimd [options]"
quit(EINVAL) # EINVAL - Invalid argument quit(EINVAL) # EINVAL - Invalid argument
setStdIoUnbuffered() # Colors don't work otherwise! setStdIoUnbuffered() # Colors don't work otherwise!
try: try:
main(logger, parseConfig(logger, "/etc/nimd/nimd.conf")) main(logger, parseConfig(logger, "/etc/nimd/nimd.conf"))