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
nimd
main
boot.sh
rootfs
packervm
test
@ -14,7 +13,5 @@ initrd*
vmlinuz-linux
vmlinuz*
debian*
start.sh
rebuild.sh
vm.qcow2
*.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
sigtermDelay = 90 # Delay (seconds) that nimd will wait before terminating child processes with
# 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
[Logging]
level = info
level = debug
logFile = /var/log/nimd
[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:
logger.info("Processing default runlevel")
startServices(logger, workers=config.workers, level=Default)
logger.debug(&"Unblocking signals")
unblockSignals(logger)
logger.debug(&"Unblocking signals")
unblockSignals(logger)
logger.info("System initialization complete, idling on control socket")
var opType: string
try:
@ -40,40 +40,49 @@ proc mainLoop*(logger: Logger, config: NimDConfig, startServices: bool = true) =
logger.switchToFile()
logger.debug("Entering accept() loop")
while true:
serverSocket.accept(clientSocket)
logger.debug(&"Received connection on control socket")
if clientSocket.recv(opType, size=1) == 0:
logger.debug(&"Client has disconnected, waiting for new connections")
continue
logger.debug(&"Received operation type '{opType}' via control socket")
# The operation type is a single byte:
# - 'p' -> poweroff
# - 'r' -> reboot
# - 'h' -> halt
# - 's' -> Services-related operations (start, stop, get status, etc.)
# - 'l' -> Reload in-memory configuration
case opType:
of "p":
logger.info("Received shutdown request")
shutdown(logger)
of "r":
logger.info("Received reboot request")
reboot(logger)
of "h":
logger.info("Received halt request")
halt(logger)
of "s":
logger.info("Received service request")
# TODO: Operate on services
of "l":
logger.info("Received reload request")
mainLoop(logger, parseConfig(logger, "/etc/nimd/nimd.conf"), startServices=false)
else:
logger.warning(&"Received unknown operation type '{opType}' via control socket, ignoring it")
discard
clientSocket.close()
try:
serverSocket.accept(clientSocket)
logger.debug(&"Received connection on control socket")
if clientSocket.recv(opType, size=1) == 0:
logger.debug(&"Client has disconnected, waiting for new connections")
continue
logger.debug(&"Received operation type '{opType}' via control socket")
# The operation type is a single byte:
# - 'p' -> poweroff
# - 'r' -> reboot
# - 'h' -> halt
# - 's' -> Services-related operations (start, stop, get status, etc.)
# - 'l' -> Reload in-memory configuration
# - 'c' -> Check NimD status (returns "1" if up)
case opType:
of "p":
logger.info("Received shutdown request")
shutdown(logger)
of "r":
logger.info("Received reboot request")
reboot(logger)
of "h":
logger.info("Received halt request")
halt(logger)
of "s":
logger.info("Received service-related request")
# TODO: Operate on services
of "l":
logger.info("Received reload request")
mainLoop(logger, parseConfig(logger, "/etc/nimd/nimd.conf"), startServices=false)
of "c":
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:
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)
# We *absolutely* cannot die
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()
var shutdownHandlers: seq[ShutdownHandler] = @[]
var sigTermDelay: float = 90
proc newShutdownHandler*(body: proc (logger: Logger, code: int)): ShutdownHandler =
result = ShutdownHandler(body: body)
var shutdownHandlers: seq[ShutdownHandler] = @[]
var sigTermDelay: float = 90
proc setSigTermDelay*(delay: int = 90) =
# Sets the sigtermDelay variable
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
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")
discard execCmd(os.getEnv("SHELL", "/bin/sh")) # TODO: Is this fine? maybe use execProcess
discard posix.kill(-1, SIGKILL)
discard execCmd(os.getEnv("SHELL", "/bin/sh")) # TODO: Is this fine? maybe use execProcess
quit(-1)
logger.warning("The system is shutting down")
logger.info("Processing shutdown runlevel")

View File

@ -14,6 +14,7 @@
import parseopt
import strformat
import posix
import net
import os
# NimD's own stuff
@ -58,9 +59,9 @@ proc addStuff =
depends=(@[newDependency(Other, errorer)]), provides=(@[]),
stdin="/dev/null", stderr="", stdout="")
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=(@[]),
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(echoer)
@ -68,10 +69,55 @@ proc addStuff =
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) =
## NimD's entry point and setup
## function
if not checkControlSocket(logger, config):
return
logger.debug(&"Setting log file to '{config.logFile}'")
setLogFile(file=config.logFile)
# Black Lives Matter. This is sarcasm btw. Fuck the left
@ -175,8 +221,7 @@ when isMainModule:
quit(EINVAL) # EINVAL - Invalid argument
else:
echo "Usage: nimd [options]"
quit(EINVAL) # EINVAL - Invalid argument
quit(EINVAL) # EINVAL - Invalid argument
setStdIoUnbuffered() # Colors don't work otherwise!
try:
main(logger, parseConfig(logger, "/etc/nimd/nimd.conf"))