mirror of https://github.com/nocturn9x/nimd.git
Merge branch 'main' of https://github.com/nocturn9x/nimd
This commit is contained in:
commit
ea1f3cc6f9
|
@ -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.*
|
||||||
|
|
|
@ -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)
|
|
@ -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]
|
||||||
|
|
|
@ -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'
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
./scripts/boot.sh --kernel vmlinuz-linux --initrd initrd-linux.img --memory 1G
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
53
src/main.nim
53
src/main.nim
|
@ -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"))
|
||||||
|
|
Loading…
Reference in New Issue