An open API service indexing awesome lists of open source software.

https://github.com/andreoss/openbsd-ex-org

Reproducible OpenBSD Installation with Org-Mode & Emacs
https://github.com/andreoss/openbsd-ex-org

openbsd org-babel org-mode

Last synced: about 1 month ago
JSON representation

Reproducible OpenBSD Installation with Org-Mode & Emacs

Awesome Lists containing this project

README

        

#+TITLE: OpenBSD Ex Org
#+AUTHOR: andreoss
#+EMAIL: [email protected]
#+LANGUAGE: en
#+KEYWORDS: openbsd emacs org-mode
#+PROPERTY: header-args :eval yes :noweb yes eval :exports results
#+PROPERTY: header-args:shell+ :shebang (a-host) :noweb yes :results output
#+HTML_HEAD: body { background-color: #ffffea; }
#+LINK_UP:
#+LINK_HOME: index.html

* Overview

This document is aimed to provide an enviroment for building a bootable OpenBSD system image, using QEMU and Org. The entire process can be automated and reproducible.
The result image will be partitioned with [[https://en.wikipedia.org/wiki/GUID_Partition_Table][GPT]] and bootable on [[https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface][UEFI]] machines.

It's possible to build an entire system automatically by "publishing" this document:

#+name: publication
#+begin_src shell :eval no
emacs --quick README.org --batch --load=init.el --funcall=org-md-export-to-markdown --kill
#+end_src

Supported OpenBSD versions:
1. 7.2
2. 7.3

* Prerequisites
The main requirements are QEMU, Expect & GNU Emacs.
Pass is used to managed passwords, and can be changed to something else (i.e literals) in the definitions below.

** Dependencies
|--------+----------|
| emacs | >= 27.2 |
| qemu | >= 6.0.0 |
| expect | >= 1.1 |
| bash | |
| pass | |

*** On Nix
See [[file:shell.nix][shell.nix]]
#+begin_src nix :tangle shell.nix
with import {};
stdenv.mkDerivation {
name = "emacs-qemu-enviroment";
buildInputs = [
pkgs.gnutar
pkgs.wget
pkgs.qemu
pkgs.coreutils
pkgs.openssh
pkgs.rsync
pkgs.expect
pkgs.emacs-nox
pkgs.netcat-gnu
pkgs.inetutils
pkgs.pass
];
}
#+end_src

*** Current versions
#+name: emacs-version
#+BEGIN_SRC emacs-lisp
(emacs-version)
#+END_SRC
#+name: qemu-version
#+BEGIN_SRC shell
qemu-kvm --version
#+END_SRC
#+name: expect-version
#+BEGIN_SRC shell
expect -version
#+END_SRC

** Lisp Variables & Functions
*** Definitions
This definitions are needed to provide dynamic values in `header-args`.

For example:
#+begin_example
,#+begin_src shell :eval (a-is-system-p 'openbsd)
zzz
,#+end_src
#+end_example

#+name: definitions
#+begin_src emacs-lisp
(defvar a-ref-cache (make-hash-table :test 'equal))
(defun a-ref (name)
(or (gethash name a-ref-cache)
(let ((value
(save-excursion
(org-babel-execute-src-block nil
(org-babel-lob-get-info
(list
'babel-call
(list
:call
"ref-unquoted"
:arguments
(concat
"'"
name))))))))
(puthash name value a-ref-cache))))
(defun a-is-system-p (sys)
"Which is the host system-p. "
(if (eq system-type sys) "yes" "no"))
(defun a-host ()
(concat "#!" default-directory "host-shell"))
(defvar a-guest (concat "#!" default-directory "guest-shell"))
(defun a-sudo ()
"Directory for executing as root. XS is sub directories. "
(concat "/sudo::/" default-directory))
(defvar a-remote
(let ((user (a-ref "user-name"))
(port (a-ref "ssh-port")))
(concat (format "/ssh:%s@localhost#%d:" user port))))
(defvar a-remote-sudo
(let ((user (a-ref "user-name"))
(port (a-ref "ssh-port")))
(concat (format "/ssh:%s@localhost#%d|sudo:" user port))))
(defun a-contains-p? (needle haystack)
(cl-assert
(cl-search needle haystack)))
(defun a-not-contains-p? (needle haystack)
(cl-assert
(not (cl-search needle haystack))))
#+end_src

*** Remove all result blocks
Clean up this document from all /result/ blocks.
#+BEGIN_SRC elisp
(defun a-ob-clear-all-results ()
"Clear all results in the buffer."
(interactive)
(save-excursion
(goto-char (point-min))
(while (org-babel-next-src-block)
(org-babel-remove-result))))
#+END_SRC

*** Rotate old log
#+name: rotate-log
#+begin_src shell
«log»
if [ -e "$LOG" ]
then
mv --verbose --force "$LOG" "${LOG//.log}$(date +%s).log"
fi
#+end_src

** Expect scripts
:PROPERTIES:
:header-args: :eval no :noweb yes no-export
:END:
*** Connect to serial port (via telnet).
#+name: serial
#+begin_src tcl
log_user 0
spawn telnet localhost «ref('serial-port)»
log_user 1
#+end_src

*** Connect to monitor port (via telnet).
#+name: monitor
#+begin_src tcl
log_user 0
spawn telnet localhost «ref('monitor-port)»
log_user 1
#+end_src

#+name: expect-prompt
#+begin_src tcl
expect -re {(?n)(?:[a-z]+)?[#]\s}
#+end_src

*** Set com0 as the main tty
Executed during boot of install.img
#+begin_src shell :tangle set-tty :shebang "#!/usr/bin/env expect"
«serial»
«timeout»
expect "boot>"
send "stty com0 115200\r"
expect "boot>"
sleep 1
send "set tty com0\r"
expect "boot>"
sleep 1
send "boot\r\r\r"
sleep 1
expect "\r"
exit
#+end_src

*** Reset VM
Executed during boot of install.img
#+begin_src shell :tangle qemu-command :shebang "#!/usr/bin/env expect"
«monitor»
«timeout»
set command [lindex $argv 0];
expect "(qemu)"
send "$command\r"
expect "(qemu)"
exit
#+end_src

*** Timeouts
**** Disable timeout
#+name: notimeout
#+begin_src tcl
set timeout -1
#+end_src
**** Enable timeout
#+name: timeout
#+begin_src tcl
set timeout 10
#+end_src
*** Start interactive shell.
#+begin_src shell :tangle start-shell :shebang "#!/usr/bin/env expect"
«serial»
«notimeout»
send "\r";
expect "(I)nstall, (U)pgrade, (A)utoinstall or (S)hell?" { send "S\r" }
«expect-prompt»
#+end_src

*** Execute guest shell command (after interactive was started).
Execute a shell command via COM.
#+begin_src shell :tangle execute :shebang "#!/usr/bin/env expect"
set command [lindex $argv 0];
«serial»
«timeout»
set send_human {.1 .3 1 .05 2}
send -h "$command"
send -h "\r"
after 1000 set stop_wait &
vwait stop_wait
unset stop_wait
«expect-prompt»
#+end_src

A wrapper for the script above to use it as part of shebang.
#+begin_src shell :tangle guest-shell
./execute "$(sed /^#!/d "$1")"
#+end_src

** Emacs configuration
*** Startup
**** Run when file is being opened
This block is executed via ~Buffer settings~ & [[file:init.el]].
#+name: startup
#+BEGIN_SRC emacs-lisp
(require 'ob-shell)
(require 'ob-eshell)
(require 'cl)
(setq org-babel-eval-verbose nil)
«definitions»
#+END_SRC

**** ANSI Colors in output
#+BEGIN_SRC elisp
(defun a-babel-ansi-color-apply ()
(when-let ((beg (org-babel-where-is-src-block-result nil nil)))
(save-excursion
(goto-char beg)
(when (looking-at org-babel-result-regexp)
(let ((end (org-babel-result-end))
(ansi-color-context-region nil))
(ansi-color-apply-on-region beg end))))))
(add-hook 'org-babel-after-execute-hook 'a-babel-ansi-color-apply)
#+END_SRC

**** Shell wrapper to capture logs
This is useful for debugging.
All code with this shebang will log its stderr & stdout to ~$LOG~.

#+name: log
#+begin_src shell :eval no
LOG=${LOG:-output.log}
#+end_src

#+name: host-shell
#+begin_src shell :tangle host-shell :shebang "#!/usr/bin/env bash" :eval no :noweb yes
«log»
if [ "$LOG" ]
then
exec 1> >(tee -a "$LOG") 2> >(tee -a "$LOG" >&2)
fi
exec "$SHELL" "$@" > /tmp/install/etc/boot.conf
echo "set tty com0" >> /tmp/install/etc/boot.conf
#+end_src

#+begin_src shell :dir (a-sudo)
umount /tmp/install
losetup --detach-all
#+end_src

**** TODO On OpenBSD
:PROPERTIES:
:header-args: :eval (a-is-system-p 'openbsd)
:END:
[[https://unix.stackexchange.com/questions/656910/how-to-change-the-installation-image-to-use-com-as-default-console][Discussion on SO]].

** Script to control VM
:PROPERTIES:
:header-args:shell: :tangle vm :eval no :tangle-mode (identity #o755) :shebang "#!/usr/bin/env bash"
:END:
Wait until port is open:
#+begin_src shell
waitport() {
while ! nc -z localhost "${1:?no argument}" ; do sleep 3; done
}
#+end_src
#+begin_src shell
QEMU_MEM=4g
QEMU_CPU=host
QEMU_PID=.pid
QEMU_COMMAND=qemu-kvm
#+end_src
Ports for Monitor and Serial console:
#+begin_src shell
QEMU_MON_PORT=«ref("monitor-port")»
QEMU_SER_PORT=«ref("serial-port")»
#+end_src

QEMU arguments:
System drive:
#+begin_src shell
QEMU_SYSTEM_DRIVE=(
-device scsi-hd,drive=hd0
-drive file=«ref("system-image")»,media=disk,snapshot=off,if=none,id=hd0,format=«ref("image-format")»
)
#+end_src
Installation drive:
#+begin_src shell
QEMU_INSTALL_DRIVE=(
-drive file=«ref("install-image")»,media=disk,format=raw
)
#+end_src
Key-disk drive:
#+begin_src shell
QEMU_KEY_DRIVE=(
-device scsi-hd,drive=hd1
-drive file=key.raw,media=disk,snapshot=off,if=none,id=hd1,format=raw
)
#+end_src
Monitor device:
#+begin_src shell
QEMU_MONITOR=(
-monitor chardev:mon0
-chardev socket,id=mon0,server=on,wait=off,telnet=on,port=$QEMU_MON_PORT,host=localhost,ipv4=on,ipv6=off
)
#+end_src
Serial device:
#+begin_src shell
QEMU_SERIAL=(
-serial chardev:ser0
-chardev socket,id=ser0,server=on,wait=off,telnet=on,port=$QEMU_SER_PORT,host=localhost,ipv4=on,ipv6=off
)
#+end_src
Network with port forwarding:
#+begin_src shell
QEMU_NETWORK=(
-netdev tap,id=mn0,br=virbr0,helper=$(type -p qemu-bridge-helper)
-device virtio-net,netdev=mn0,mac=00:00:00:00:00:01
)
#+end_src

#+begin_src shell
QEMU_SOUND=(
-audio pipewire,model=es1370,out.frequency=48000
)
#+end_src

#+begin_src shell
QEMU_OPTS=(
-vnc :0
-smp 3
-cpu phenom
-display none
-vga virtio
-m "$QEMU_MEM"
-cpu "$QEMU_CPU"
-bios bios/ovmf-x64/OVMF-pure-efi.fd
-device virtio-scsi-pci,id=scsi
)
QEMU_OPTS+=("${QEMU_NETWORK[@]}")
QEMU_OPTS+=("${QEMU_MONITOR[@]}")
QEMU_OPTS+=("${QEMU_SERIAL[@]}")
QEMU_OPTS+=("${QEMU_SYSTEM_DRIVE[@]}")
QEMU_OPTS+=("${QEMU_SOUND[@]}")
QEMU_OPTS+=(-pidfile "$QEMU_PID" -daemonize)
__boot() {
./type-password «pass-show("bioctl")»
}
__status() {
if [ ! -e "$QEMU_PID" ]
then
>&2 echo "Not running"
exit 1
fi
PID="$(< "$QEMU_PID")"
if kill -0 "$PID" >/dev/null 2>/dev/null
then
>&2 echo "Running: $PID"
else
>&2 echo "Stopped: $PID"
exit 1
fi
}
__start() {
[ -e "$QEMU_PID" ] && >&2 echo "Already running" && exit 1
"$QEMU_COMMAND" "${QEMU_OPTS[@]}"
waitport "$QEMU_MON_PORT"
waitport "$QEMU_SER_PORT"
while :
do
PID=$(cat < "$QEMU_PID")
kill -0 $PID && echo Running && break
done
}
if [ "${USE_KEYDISK:-0}" -eq "1" ]
then
QEMU_OPTS+=("${QEMU_KEY_DRIVE[@]}")
fi
if [ "${USE_INSTALL:-1}" -eq "1" ]
then
QEMU_OPTS+=("${QEMU_INSTALL_DRIVE[@]}")
fi
case "${1:?no arg}" in
start)
__start
__boot
;;
status)
__status
;;
reset)
__status
./qemu-command system_reset
;;
poweroff)
__status
./qemu-command system_powerdown
;;
stop)
[ -e "$QEMU_PID" ] && xargs kill < "$QEMU_PID"
rm --force "$QEMU_PID"
;;
restart)
"$0" stop
"$0" start
;;
esac
#+end_src

* Qemu
** Setup UEFI Bios
*** UEFI Bios image
Installing [[https://github.com/tianocore/tianocore.github.io/wiki/OVMF][UEFI Bios]] for QEMU.
This BIOS does not support CD, this is why we are using a USB image.

#+begin_src shell
wget --continue https://packages.slackonly.com/pub/packages/14.2-x86_64/system/ovmf/ovmf-20171116-noarch-1_slonly.txz
#+end_src

#+begin_src shell
tar -C ./bios -xvf ovmf*txz --strip-components=2
#+end_src

** Prepare image
*** Main volume
#+name: qemu-img
#+begin_src shell :post assert!(*this*, '(a-contains-p? "Formatting" block))
qemu-img create -f «ref("image-format")» «ref("system-image")» «ref("volume-size")»
#+end_src

* Instalation
** Start QEMU & set TTY to com0
Stop VM:
#+NAME: stop-qemu
#+begin_src shell
./vm status
./vm stop
#+end_src

#+RESULTS: stop-qemu

Start VM:
#+NAME: start-qemu
#+begin_src shell :prologue exec 0>&- 1>&- 2>&- :dir "/sudo::"
env USE_GRAPHIC=1 USE_INSTALL=0 ./vm start
#+end_src

#+RESULTS: start-qemu

#+NAME: boot-install
#+begin_src shell :post assert!(*this*, '(a-contains-p? "switching console to com0" block))
./set-tty
#+end_src

** Start interactive shell
#+NAME: start-shell
#+begin_src shell :post assert!(*this*, '(a-contains-p? "#" block))
./start-shell
#+end_src

** Check available disks (sd0 & wd0 should present)
Print names of available disks:
#+name: check-disknames
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "hw.disknames=" block))
sysctl hw.disknames
#+end_src

You should see the target image being attached as ~sd0~.
#+name: check-sd0
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "sd0 at" block))
dmesg | grep sd[0-9]
#+end_src

Installation media should be available as ~wd0~ (if installing from img file)
#+name: check-wd0
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "wd0 at" block))
dmesg | grep wd[0-9]
#+end_src

** Prepare disk
*** Create devices for sd0 and sd1
#+name: create-devices
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "sd0a" block))
cd /dev
sh MAKEDEV sd0
sh MAKEDEV sd1
sh MAKEDEV sd2
ls -l sd*a
#+end_src

*** Remove disk content
#+name: shred-gpt
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "bytes transferred" block))
dd if=/dev/zero of=/dev/rsd0c bs=1m count=100
#+end_src

*** Run fdisk
#+name: fdisk-sd0
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "Writing GPT." block))
fdisk -iy -g -b 960 sd0
#+end_src

The same for keydisk (Optional)
#+name: fdisk-sd1
#+begin_src shell :shebang (eval 'a-guest) :eval (if (org-entry-get nil "use-key-disk" t) "yes" "no")
fdisk -iy -g -b 960 sd1
#+end_src

*** Disklabel
Create one RAID partition using entire disk space.
#+name: disklabel-sd0
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "partitions" block))
{
echo a a
echo
echo
echo raid
echo w
echo q
} | disklabel -E sd0
disklabel sd0
#+end_src

Prepare keydisk (Optional)
#+name: Disklabel on sd1
#+begin_src shell :shebang (eval 'a-guest) :eval (if (org-entry-get nil "use-key-disk" t) "yes" "no") :results verbatim
{
echo a a
echo
echo
echo raid
echo w
echo q
} | disklabel -E sd1
disklabel sd1
#+end_src
*** Create [[https://man.openbsd.org/bioctl][bioctl(8)]] Crypto RAID

**** Put passphase in a file
NOTE: New line at EOF is required.
#+name: pass-file
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "#" block))
echo «pass-show("bioctl")» > /tmp/.passphrase
#+end_src

#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "-rw-------" block))
chmod 0600 /tmp/.passphrase
ls -l /tmp/.passphrase
#+end_src

Initialize RAID on sd0
#+name: bioctl-passphrase
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "softraid0:" block))
bioctl -p /tmp/.passphrase -c C -l sd0a softraid0
#+end_src

Using keydisk (Optional)
#+name: bioctl-keydisk
#+begin_src shell :shebang (eval 'a-guest) :eval (if (org-entry-get nil "use-key-disk" t) "yes" "no") :results verbatim
bioctl -k sd1a -c C -l sd0a softraid0
#+end_src

** Main setup
*** Setup dialog
:PROPERTIES:
:header-args: :eval no :noweb yes :tangle setup-dialog
:END:

Send ^D and press enter.
#+begin_src tcl :shebang "#!/usr/bin/env expect"
«serial»
set send_human {.1 .3 1 .05 2}
send "\x04"
send "\r"
#+end_src

#+begin_src tcl
expect "(I)nstall, (U)pgrade, (A)utoinstall or (S)hell?" { send "I\r" }
#+end_src

#+begin_src tcl
expect "Terminal type?" { send "vt220\r" }
#+end_src

#+begin_src tcl
expect "System hostname?" { send «ref("hostname")»; send "\r" }
#+end_src

Do not configure network interfaces.
#+begin_src tcl
expect "Network interface to configure?" {
send "done\r"
}
#+end_src

DNS Domain name.
#+begin_src tcl
expect "DNS domain name?" {
send «ref("domain")»;
send "\r";
}
#+end_src
DNS Domain name.
#+begin_src tcl
expect "DNS nameservers?" {
send "1.1.1.1\r";
}
#+end_src

Root password.
#+begin_src tcl
expect "Password for root account?" {
sleep 1
send -h «ref("root-password")»
send "\r"
}
expect "Password for root account? (again)" {
sleep 1
send -h «ref("root-password")»
send "\r"
}
#+end_src

Do not start sshd(8) by default yet. Will be enabled later.
#+begin_src tcl
expect "Start sshd(8) by default?" {
send "no\r"
}
#+end_src

Do not start xenodm(1) by default yet. Will be enabled later.
#+begin_src tcl
expect "Do you want the X Window System to be started by xenodm(1)?" {
send "no\r"
}
#+end_src

Keep COM0 available after reboot to the freshly installed system.
Will be disabled after sshd(8) is enabled.
#+begin_src tcl
expect "Change the default console to com0?" {
send "yes\r"
expect "Which speed should com0 use?" {
send "115200\r"
}
}
#+end_src

No need to add a user at this step.
#+begin_src tcl
expect "Setup a user?" {
send "no\r"
}
#+end_src

#+begin_src tcl :tangle no
expect "Allow root ssh login?" {
send "no\r"
}
#+end_src

#+begin_src tcl
expect "Which disk is the root disk?" {
send "sd1\r"
}
#+end_src

#+begin_src tcl
expect "Use (W)hole disk MBR, whole disk (G)PT" {
send "gpt\r"
}
#+end_src

Use custom layout, use entire volume as /.
#+begin_src tcl
expect "Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout?" {
send "c"
send "\r"
expect ">" { send "a a" ; send "\r" }
expect "offset:" { send "\r" }
expect "size:" { send "90%" ; send "\r" }
expect "FS type:" { send "\r" }
expect "mount point:" { send "/" ; send "\r" }
expect ">" { send "a b" ; send "\r" }
expect "offset:" { send "\r" }
expect "size:" { send "*" ; send "\r" }
expect "FS type:" { send "swap" ; send "\r" }
expect "*>" { send "w" ; send "\r" }
expect ">" { send "p" ; send "\r" }
expect ">" { send "q" ; send "\r" }
}
#+end_src

(Alternative) Use automatic layout, which produces different results depending on volume size.
#+begin_src tcl :tangle no
expect "Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout?" {
send "a\r"
}
#+end_src

#+begin_src tcl
expect "Which disk do you wish to initialize?" {
send "done\r"
}
#+end_src

#+begin_src tcl
expect "Location of sets?" {
send "disk\r"
}
#+end_src

#+begin_src tcl
expect "Is the disk partition already mounted?" {
send "no\r"
}
#+end_src

Install from `wd0`, which is USB installation media.
#+begin_src tcl
expect "Which disk contains the install media?" {
send "wd0\r"
}
#+end_src

#+begin_src tcl
expect "Which wd0 partition has the install sets?" {
send "a\r"
}
#+end_src

#+begin_src tcl
expect "Pathname to the sets?" {
send "\r"
}
#+end_src

Install everything but games.
#+begin_src tcl
expect "Set name(s)?" {
send -- "-game*\r\r"
}
#+end_src

There is no SHA256.sig on the installation drive.
This step will triger installation, thus "notimeout".
#+begin_src tcl
expect "Continue without verification?" {
send "yes\r"
«notimeout»
}
#+end_src

#+begin_src tcl
expect "Location of sets? (disk http nfs or 'done')" {
send "done\r"
}
#+end_src

#+begin_src tcl
expect "What timezone are you in?" {
send «ref("time-zone")»;
send "\r";
}
#+end_src

Not ready to reboot yet. Go back to the shell to install UEFI.
#+begin_src tcl
expect "Exit to (S)hell, (H)alt or (R)eboot?" {
send "S\r"
«expect-prompt»
}
#+end_src

*** Start setup
#+name: setup-dialog
#+begin_src shell :post assert!(*this*, '(a-contains-p? "CONGRATULATIONS!" block))
./setup-dialog
#+end_src

** Install UEFI Boot Loader
*** Mount partition & copy EFI
#+name: Format UEFI Parition
#+name: format-uefi-partition
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "block device" block))
newfs_msdos /dev/sd0i
#+end_src

#+name: copy-uefi
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "/mnt2 type msdos" block))
mount /dev/sd0i /mnt2
cp /mnt/usr/mdec/BOOTX64.EFI /mnt2/efi/boot/
mount
umount /dev/sd0i
#+end_src
** Mount /tmp as mfs
#+begin_src shell :shebang (eval 'a-guest)
echo 'swap /tmp mfs rw,nodev,nosuid,-s=300m 0 0' >> /mnt/etc/fstab
chmod 1777 /mnt/tmp
#+end_src

** Reboot
#+name: reboot after install
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "#" block))
halt
#+end_src

** Stop VM
#+name: stop vm
#+begin_src shell
sleep 5
./vm stop
#+end_src

* Login into the new system
Start VM without the installation media, and type cryptodisk password:
#+name: start-vm
#+begin_src shell :prologue exec 0>&- 1>&- 2>&-
./vm stop
USE_INSTALL=0 ./vm start
#+end_src

#+name: type-bioctl-password
#+begin_src shell :post assert!(*this*, '(a-contains-p? "boot>" block))
./type-password «pass-show("bioctl")»
#+end_src

Login as root via COM
#+name: login-as-root
#+begin_src shell :post assert!(*this*, '(a-contains-p? "Welcome to OpenBSD" block))
./login root «ref("root-password")»
#+end_src

* Post-install (Serial)
** Tcl scripts
:PROPERTIES:
:header-args: :eval no :noweb yes
:END:
*** Crypto-disk password
#+begin_src shell :tangle type-password :shebang "#!/usr/bin/env expect"
«serial»
set password [lindex $argv 0];
«timeout»
expect "boot>" { send "set device sr0a\r" }
expect "boot>" { send "\r" }
expect "Passphrase: " { send "$password\r" }
expect "boot>" { send "machine gop 15\r" }
expect "boot>" { send "boot /bsd\r" }
expect "booting"
#+end_src

*** Login via tty0
#+begin_src shell :tangle login :shebang "#!/usr/bin/env expect"
«notimeout»
«serial»
set user [lindex $argv 0];
set password [lindex $argv 1];
send "\r\r\r"
expect "login:"
send "$user\r"
sleep 1
expect "Password:"
send "$password\r"
«expect-prompt»
sleep 1
#+end_src

** Add a normal user
*** Tcl script
:PROPERTIES:
:header-args: :tangle adduser :noweb yes :eval no
:END:
#+begin_src tcl :shebang "#!/usr/bin/env expect" :tangle-mode (identity #o755)
«serial»
send "\r"
«expect-prompt»
send "adduser\r"
#+end_src

If /etc/adduser.conf doesn't exits...
#+begin_src tcl
expect "Couldn't find /etc/adduser.conf" {
expect "Enter your default shell:" { send "ksh\r"; }
expect "Default login class:" { send "default\r"}
expect "Enter your default HOME partition:" { send "/home\r"; }
expect "Copy dotfiles from:" { send "/etc/skel\r"; }
expect "Send welcome message?" { send "no\r"; }
expect "Prompt for passwords by default" { send "no\r"; }
expect "Default encryption method for passwords:" { send "blowfish\r" }
}
#+end_src
New user
#+begin_src tcl
expect "Enter username" { send «ref('user-name)» ; send "\r" }
expect "Enter full name" { send "\r" }
expect "Enter shell" { send "ksh\r" }
expect "Uid" { send «ref('user-id)» ; send "\r" }
expect "Login group" { send «ref('user-group)» ; send "\r" }
expect "Invite a into other groups" { send "no\r" }
expect "Login class" { send "default\r" }
expect "OK?" { send "y\r" }
expect "Add another user?" { send "n\r" }
«expect-prompt»
#+end_src

*** Add user
#+name: add-user
#+begin_src shell :post assert!(*this*, '(a-contains-p? "Added user" block))
./adduser
#+end_src

*** Configure [[https://man.openbsd.org/doas.8][doas(8)]]
Disable password promt for staff group.
See [[https://man.openbsd.org/doas.conf.5][doas.conf(5)]]

#+name: configure-doas
#+begin_src shell :shebang (eval 'a-guest) :post assert!(*this*, '(a-contains-p? "#" block))
echo permit nopass :«ref("user-group")»| tee /etc/doas.conf
#+end_src

** Configure SSH
Change default parameters of [[https://man.openbsd.org/sshd][sshd(8)]]
*** Backup original config
#+name: backup-sshd_config
#+begin_src shell :shebang (eval 'a-guest)
cp /etc/ssh/sshd_config{,.orig}
#+end_src

** Disable motd
#+begin_src shell :shebang (eval 'a-guest)
rm /etc/motd
ln -s /dev/null /etc/motd
#+end_src

*** Disable banner
#+name: configure-sshd-1
#+begin_src shell :shebang (eval 'a-guest)
perl -i -pE 's/[#]?(Banner) \s+ \S+/$1 none/x' /etc/ssh/sshd_config
perl -i -pE 's/[#]?(PrintMotd) \s+ \S+/$1 no/x' /etc/ssh/sshd_config
#+end_src

*** Disable/enable X11 Forwading
#+name: configure-sshd-2
#+begin_src shell :shebang (eval 'a-guest)
perl -i -pE 's/[#]?(X11Forwarding) \s+ \S+/$1 yes/x' /etc/ssh/sshd_config
#+end_src

*** Disable password authentication & root login
#+name: configure-sshd33
#+begin_src shell :shebang (eval 'a-guest)
perl -i -pE 's/[#]?(PasswordAuthentication) \s+ \S+/$1 no/x' /etc/ssh/sshd_config
perl -i -pE 's/[#]?(PermitRootLogin) \s+ \S+/$1 no/x' /etc/ssh/sshd_config
#+end_src

*** Enable [[https://man.openbsd.org/sshd][sshd(8)]]
#+name: enable-sshd
#+begin_src shell :shebang (eval 'a-guest)
rcctl enable sshd
rcctl restart sshd
#+end_src

*** Add RSA key
#+name: ssh-key
#+begin_src emacs-lisp :post assert!(*this*, '(a-not-contains-p? "The agent has no identities." block))
(string-trim (shell-command-to-string "ssh-add -L"))
#+end_src

#+name: add-ssh-key
#+begin_src shell :shebang (eval 'a-guest) :noweb yes :var user=ref-unquoted('user-name)
echo «ssh-key()» | doas -u a tee /home/$user/.ssh/authorized_keys
#+end_src

*** Enable network
**** Use rcctl & /etc/hostname.vio0 instead of dhclient
#+name: Enable sshd
#+begin_src shell :shebang (eval 'a-guest)
{
echo "-inet6"
echo "dhcp"
echo "up"
} | tee /etc/hostname.vio0
#+end_src

#+name: temporary-dns
#+begin_src shell :shebang (eval 'a-guest)
perl -i -pE 's/(nameserver) \s+ \S+/$1 8.8.8.8/x' /etc/resolv.conf
#+end_src

#+name: netstart
#+begin_src shell :shebang (eval 'a-guest)
sh /etc/netstart
#+end_src

*** Add fingerprint to [[~/.ssh/known_hosts][known_hosts]]
#+name: ssh-keyscan
#+begin_src shell :shebang (a-host)
ssh-keyscan -p «ref("ssh-port")» -H localhost | tee -a ~/.ssh/known_hosts
#+end_src

* Post-install (SSH)
:PROPERTIES:
:header-args: :dir (eval 'a-remote) :session *post-install* :noweb yes eval :exports both :cache no
:END:
** Configure sudo
Tramp does not support [[https://man.openbsd.org/doas][doas(8)]].
Let's install & configure sudo with the same permissions as doas.

#+name: install-sudo
#+call: pkg_add("sudo--")

#+name: sudoers
#+begin_src shell :var group=(a-ref "user-group")
echo "%$group ALL=(ALL) NOPASSWD: SETENV: ALL" | doas tee /etc/sudoers
#+end_src

** Update firmware
#+name: firmware
#+begin_src shell
doas fw_update -a
doas fw_update iwm
doas fw_update iwi
doas fw_update iwn
doas fw_update iwx
#+end_src
** Disable com0 at boot
#+name: disable-com0
#+begin_src shell
doas perl -i.bak -nE 'print unless /com0/' /etc/boot.conf
#+end_src

** Fix resolution for QEMU
#+begin_src shell
echo "machine gop 15" | doas tee -a /etc/boot.conf
#+end_src

** wscons
#+begin_src shell
echo 'keyboard.bell.volume=0' | doas tee /etc/wsconsctl.conf
echo 'keyboard.map+="keysym Caps_Lock = Control_L"' | doas tee -a /etc/wsconsctl.conf
echo 'display.screen_off=60000' | doas tee -a /etc/wsconsctl.conf
#+end_src
** sysctl
#+begin_src shell
echo 'vm.swapencrypt.enable=1' | doas tee /etc/sysctl.conf
echo 'machdep.lidaction=2' | doas tee -a /etc/sysctl.conf
echo 'machdep.pwraction=1' | doas tee -a /etc/sysctl.conf
#+end_src
** ntpd
#+begin_src shell
doas rcctl disable ntpd
doas rcctl stop ntpd
#+end_src
** noatime softdep
#+begin_src shell
doas perl -i.bak -pE 's/(?<=rw)(?!,noatime)/,noatime/' /etc/fstab
doas perl -i.bak -pE 's/(?<=rw)(?!,softdep)/,softdep/' /etc/fstab
#+end_src

** Fix audio in QEMU
#+name: sndiod
#+begin_src shell
doas rcctl set sndiod flags -b24000
doas rcctl restart sndiod
#+end_src

* Configuration
:PROPERTIES:
:header-args: :dir (eval 'a-remote) :session *configuration* :noweb yes :exports both :cache no
:END:
** Install packages
Unlock database in case it's locked (see pkg_check(8)).

#+name: pkg_check
#+begin_src shell
doas pkg_check -f
#+end_src

Install a package (see pkg_add(8)).
#+name: pkg_add
#+begin_src shell :var NAME=""
[ "$NAME" ] && doas pkg_add -x -r "$NAME"
#+end_src

*** Emacs
#+call: pkg_add("git")

#+call: pkg_add("gnupg")

#+call: pkg_add("emacs--gtk3")

#+call: pkg_add("wget")

*** Firefox
#+call: pkg_add("firefox")

#+call: pkg_add("mpv")

#+call: pkg_add("youtube-dl")

#+begin_src shell
UA_FIX=https://gist.githubusercontent.com/andreoss/91a0d21dc99bd9eae8bce5d573fb5a00/raw/64d0926caf60103efab55ea4cc4150d5d86b369a/ua.js
SER_JS=https://raw.githubusercontent.com/pyllyukko/user.js/master/user.js
FIREFOX_BASE=/usr/local/lib/firefox
doas cp "$FIREFOX_BASE"/browser/defaults/preferences/all-openbsd.js{,.back}
curl "$USER_JS" | grep -v captive-portal | \
doas tee -a "$FIREFOX_BASE"/browser/defaults/preferences/all-openbsd.js >/dev/null
curl "$UA_FIX" | doas tee -a "$FIREFOX_BASE"/browser/defaults/preferences/all-openbsd.js
doas sed -i s/user_pref/pref/ "$FIREFOX_BASE"/browser/defaults/preferences/all-openbsd.js
#+end_src

** Switch to prefered shell (i.e Bash)

#+call: pkg_add("bash")

#+begin_src shell :var XSHELL=ref-unquoted("user-shell")
chsh -s `which $XSHELL`
#+end_src

*** Switch back to Korn Shell
#+begin_src shell :eval no
chsh -s "/bin/ksh"
#+end_src

** Enable apmd
- ~-A~ :: enables performance adjustment mode
- ~-Z 5~ :: hibernate at 5% battery life
See [[https://man.openbsd.org/apmd][apmd(8)]].

#+begin_src shell
doas mkdir /etc/apm
echo "#!/bin/sh" | doas tee /etc/apm/suspend
echo "pkill -USR1 xidle" | doas tee -a /etc/apm/suspend
doas chmod +x /etc/apm/suspend
#+end_src

#+begin_src shell
doas rcctl enable apmd
doas rcctl set apmd flags -A -Z 5
doas rcctl start apmd
doas rcctl check apmd
#+end_src

** Readline
#+begin_src fundamental
$include /etc/inputrc
set bell-style visible

set blink-matching-paren on
set visible-stats on

$if mode=vi

set editing-mode vi
set keymap vi
set vi-cmd-mode-string "*"
set vi-ins-mode-string " "
set show-mode-in-prompt on

Control-l: clear-screen

set keymap vi-command
Control-l: clear-screen

set keymap vi-insert
Control-l: clear-screen

$endif

set emacs-mode-string "&"
#+end_src

** DNS Crypt Proxy

#+call: pkg_add("dnscrypt-proxy--")

*** Enable and start service
#+begin_src shell
doas rcctl enable dnscrypt_proxy
doas rcctl start dnscrypt_proxy
doas rcctl check dnscrypt_proxy
#+end_src

*** Configure dhclient
See ~man dhclient.conf~.
#+begin_src shell
echo "supersede domain-name-servers 127.0.0.1;" | doas tee /etc/dhclient.conf
#+end_src

Restart network
#+begin_src shell
doas sh /etc/netstart
#+end_src

Now resolv.conf should contains-p local DNS server
#+begin_src shell
grep nameserver /etc/resolv.conf
#+end_src

** Configure X11
#+call: pkg_add("xcape--")
#+call: pkg_add("terminus-font--centered_tilde")
#+call: pkg_add("unifont--")
#+call: pkg_add("amigafonts--")
#+call: pkg_add("rxvt-unicode--everything")
*** Autologin
#+begin_src shell :noweb yes
echo "DisplayManager.*.autoLogin: «ref('user-name)»" | doas tee -a /etc/X11/xenodm/xenodm-config
#+end_src

*** Enable

#+name: enable-xenodm
#+begin_src shell
doas rcctl enable xenodm
doas rcctl start xenodm
doas rcctl check xenodm
#+end_src

** Window manager
*** Ratpoison

#+call: pkg_add("ratpoison")

#+begin_src shell :post assert!(*this*, '(a-contains-p? "/usr/local/bin/ratpoison" block))
which ratpoison
#+end_src

#+begin_src shell
echo "exec ratpoison" > ~/.xsession
#+end_src

*** StumpWM
:PROPERTIES:
:header-args: :eval no
:END:
**** Common Lisp
***** Complier
#+call: pkg_add("sbcl--threads")
***** Quicklisp
https://www.quicklisp.org/beta-

#+begin_src shell
curl -O https://beta.quicklisp.org/quicklisp.lisp
curl -O https://beta.quicklisp.org/quicklisp.lisp.asc
#+end_src

#+begin_src shell
gpg --keyserver pgp.mit.edu --recv-keys 028B5FF7
#+end_src

#+begin_src shell
gpg --verify quicklisp.lisp.asc quicklisp.lisp
#+end_src

#+begin_src shell
sbcl --non-interactive --load quicklisp.lisp --eval "(quicklisp-quickstart:install)"
#+end_src

Add Quicklisp to .sbclrc
#+begin_src shell
sbcl --non-interactive --load quicklisp/setup.lisp --eval "(ql-util:without-prompting (ql:add-to-init-file))"
#+end_src

**** Install StumpWM

#+call: pkg_add("openbsd-backgrounds")

#+begin_src shell
sbcl --non-interactive --eval "(ql:quickload :bt-semaphore)"
#+end_src

#+begin_src shell
sbcl --non-interactive --eval "(ql:quickload :external-program)"
#+end_src

#+begin_src shell
sbcl --non-interactive --eval "(ql:quickload :swank)"
#+end_src

#+begin_src shell
sbcl --non-interactive --eval "(ql:quickload :stumpwm)"
#+end_src

#+begin_src shell :dir (eval 'a-remote)
git clone https://github.com/andreoss/.stumpwm.d
#+end_src

#+begin_src shell
echo "exec ~/.stumpwm.d/start.sh" > ~/.xsession
#+end_src

**** Miscellaneous

#+call: pkg_add("rxvt-unicode")

#+begin_src shell :post assert!(*this*, '(a-contains-p? "/usr/local/bin/urxvt" block))
which urxvt
#+end_src

** Configure shell

#+begin_src shell
git clone https://github.com/andreoss/.config
#+end_src

#+begin_src shell
ln -s ~/.config/shrc ~/.kshrc
ln -s ~/.config/shrc ~/.bashrc
ln -s ~/.config/shrc ~/.bash_profile
ln -s ~/.config/inputrc ~/.inputrc
#+end_src

** Configure Emacs
#+begin_src shell
git clone https://github.com/andreoss/.emacs.d
#+end_src

#+call: pkg_add("poppler")

#+call: pkg_add("ImageMagick")

#+call: pkg_add("gtar--static")

#+call: pkg_add("aspell")

#+call: pkg_add("coreutils")

** Misc
#+call: pkg_add("rsync--")

#+call: pkg_add("p5-ack")

** Media
#+call: pkg_add("mpv--")

#+call: pkg_add("yt-dlp--")

#+call: pkg_add("vlc--")

** Tools

#+call: pkg_add("git--")
#+call: pkg_add("cmake--")
#+call: pkg_add("clang-tools-extra--")
#+call: pkg_add("gmake--")
#+call: pkg_add("ghc--")

** (Optional) Migrate current configuration

#+call: pkg_add("rsync--")
#+begin_src shell :post assert!(*this*, '(a-contains-p? "/usr/local/bin/rsync" block))
which rsync
#+end_src

*** SSH
#+begin_src shell :dir ~ :noweb yes :session no
rsync -Lahv -e 'ssh -p «ref('ssh-port)»' ~/.ssh localhost:
#+end_src

*** GPG

#+begin_src shell :dir ~ :noweb yes :session no
rsync -Lahv -e 'ssh -p «ref('ssh-port)»' ~/.gnupg localhost:
#+end_src

*** Pass

#+begin_src shell :dir ~ :noweb yes :session no
rsync -Lahv -e 'ssh -p «ref('ssh-port)»' "$PASSWORD_STORE_DIR" localhost:
#+end_src

** TODO Adblock with unbound

* Extra
** Build Emacs
#+begin_src sh :results output :tangle build-emacs.sh
#!/bin/sh
mkdir -p ~/src
cd ~/src
if [ ! -d emacs ]; then git clone https://github.com/emacs-mirror/emacs; fi
cd ~/src/emacs
git pull
gmake clean
AUTOCONF_VERSION=2.65
CC=egcc
MAKEINFO='/usr/local/bin/gmakeinfo'
export AUTOCONF_VERSION MAKEINFO CC
./autogen.sh
./configure --prefix=$HOME/.local --with-json --with-x=lucid
gmake bootstrap
gmake
gmake install
#+end_src

* COMMENT Buffer settings
Same as [[file:init.el][init.el]]
It's impossible to use tangling here.

# Local Variables:
# org-babel-noweb-wrap-start: "«"
# org-babel-noweb-wrap-end: "»"
# org-use-property-inheritance: t
# org-confirm-babel-evaluate: nil
# eval: (require 'ob-shell)
# eval: (progn (org-babel-goto-named-src-block "startup") (org-babel-execute-src-block) (outline-hide-sublevels 1))
# End: