這篇文章是針對宿主 Ubuntu 18.04, 容器 ArchLinux 而言, 但是我認爲這也會發生在同
樣是滾動更新的 Fedora 上.
和之前的文章一樣, 沒有 md 版本. 最多隻有嵌入到文章裏的腳本編碼.
相關命令, 出於便於理解, 減少混淆的角度出發, 是能用長參數, 就用長參數.
使用此方法, 風險自擔.
起因
目前發現, ArchLinux 在 glibc 升級到至少是 2.33-3 這個版本後, 通過 Ubuntu 18.04
的
systemd-nspawn --directory /IMG_DIR
進入 bash 後, 無法通過 pacman 進行更新. 直接運行 pacman 即可看
到存在錯誤提示
error: failed to initialize alpm library
(could not find or read directory: /var/lib/pacman/)
更準確來說, 是 pacman 更新成功完成後, 立即發生, 不用退出 systemd-nspawn, 再重新
進入. 只要執行
pacman
就可出現上出錯誤提示.
如果單單從提示看是一個名爲 alpm 庫的原因, 或者是錯誤修改了 /var/lib/pacman 目錄
訪問權限.
執行
test -r /; echo $?
返回代碼爲 1, 而不是期望的 0. 知道這個方法, 是一開始自己以爲是有哪個 so 文件有
問題, 所以用 ldd 查看, 結果報錯. 而 ldd 是一個腳本, 所以在裏面開 set -x, 然後就
發現了.
這裏可以看出問題, 可能與 so 文件損壞或者錯誤的目錄權限設置無關.
用
strace -c pacman
strace pacman 2>&1 | less
可以看到一個名爲 faccessat2 的系統調用報錯. 據此在網上搜索, 可看到在 20 年 8 月
有一個關於此的討論
Newer Glibc use faccessat2 to implement faccessat #16739
https://github.com/systemd/systemd/pull/16739
我沒有查看較老版本 systemd-nspawn 是如何處理宿主系統不支持 syscall 的代碼. 也不
知道較老版本的 Ubuntu LTS 是否會 backport 到 systemd-nspawn. 所以我這裏選擇了兩
種變通解決方案.
補充說明:
systemd 環境下, 查看當前系統支持哪些 syscall
systemd-analyze syscall-filter
在 Ubuntu 18.04 所支持的兩種內核都存在此問題, 4.15 和 hwe 內核.
可通過
pacman --query --list glibc | less
dpkg-query --listfiles libc6-dev | less
查看 c header 文件, 來確認當前系統的 glibc 是否支持特定 syscall 的調用.
變通解決方案
無論是哪種方法, 其核心有很大部分是容器內的相關包, 自己做了 fallback. 如果其強制
指定用新 syscall, 則兩種方法皆失效.
第一種方案
使用 systemd-chroot, 把 ArchLinux 的更新制作成宿主系統的一個 .serivce
文件.
因爲在我之前的文章裏, 使用此方案來運行一些 ArchLinux 的軟件, 加上使用
systemd-nspawn 是進行 ArchLinux 更新, 所以自然而然選擇了這種方式.
具體配置方法, 參照 man systemd.service, man systemd.unit, man systemd.kexec 等
manual, 或者 man -k systemd, 也包括我之前的文章.
這裏只說明一個關鍵的地方. 需要鏡像裏的
/etc/mtab
文件, 存在關於鏡像 / 目錄的定義. 從自己簡單的測試看, 只要有一條這個定義就可以完
成更新. 添加該定義的一種簡單方法, 是先用
systemd-nspawn --directory /IMG_DIR
然後在容器內執行
findmnt /
根據輸出內容手動在容器的 /etc/mtab 填寫相應的內容. 或者在容器外通過
findmnt /IMG_DIR
獲得相關信息, 然後通過腳本生成相關信息給容器.
然後在容器內的適當位置放置一個腳本, 比如是 /usr/local/bin 目錄, 該腳本用於更新.
在更新前, 如果有報錯, 可能需要先執行
pacman-db-upgrade
修復 pacman 的數據庫. 此命令不需要添加額外參數.
用這種方法, 看結果比較麻煩, 我是在宿主系統用 journalctl 看的, 而且不好交互.
針對這種方法, 補充兩個用於更新的關鍵 pacman 命令參數
pacman --sync --refresh --sysupgrade --noconfirm
更新數據庫以及包. 使用長參數, 這樣即便是初次接觸 Arch 的用戶應該也能一定程度知
曉命令含義.
echo -e 'y\ny\n' | pacman --sync --clean --clean
自動清理緩存.
可考慮通過分析第一條命令的執行結果, 來自動執行第二個命令.
第二種方案
基於自己之前看過的一些文章. Linux 發行版不像 Windows 那樣系統底層集
成了可以控制任意進程是否具有入站/出站的防火牆功能. 我看到有人通過
unshare
nsenter
這些工具, 用不太方便不太優雅的方式實現了這個需求.
這裏我不限制容器內進程訪問網絡的需求, 而是儘量限制容器以 root 運行進程的可能
對宿主系統造成潛在影響. 簡單來說, 就是通過 unshare/nsenter 結合 mount/bind 仿造
systemd-nspawn 限制容器內以 root 方式運行的進程.
這個需求靠兩個腳本實現. 一個位於宿主系統, 一個位於容器內.
必須強調的是, 腳本我寫的簡單, 沒有明確提示用戶此時是在宿主系統還是容器. 所以操
作時務必小心, 以免操作錯對象, 導致不必要的損失.
相關 unshare/nsenter 的說明, 參見其 manual.
首先是宿主系統的腳本. 我起的名字是 enter-arch-apps
#!/bin/bash
set -o nounset
export IFS=$'\n'
declare -r IMG_DIR='/entry/arch/apps/rw'
declare -r UTS_FILE='/entry/arch/apps/misc/arch.image'
declare -r HOST_NAME='arch.image'
function prepare()
{
findmnt --mountpoint ${IMG_DIR}'/sys' &> /nul
[ $? -ne 0 ] && mount /sys ${IMG_DIR}'/sys' --options=bind,private,ro
}
function unmount-ns()
{
while [ 0 ];
do
umount ${UTS_FILE} 2> /nul
[ $? -ne 0 ] && break
done
while [ 0 ];
do
umount ${IMG_DIR}'/sys' 2> /nul
[ $? -ne 0 ] && break
done
}
function finalize()
{
unmount-ns
}
trap finalize EXIT
function main()
{
[ ${UID} -ne 0 ] && return 1
[ ! -f ${IMG_DIR}'/etc/os-release' ] && return 1
[ ! -f ${UTS_FILE} ] && return 1
prepare
unshare --uts=${UTS_FILE} hostname ${HOST_NAME}
nsenter --uts=${UTS_FILE} unshare --ipc --pid --mount --cgroup --fork \
chroot ${IMG_DIR} /usr/local/bin/unshare-shell
return $?
}
main
這裏簡單解釋一下:
1. 因爲我想指定容器內系統的名字, 所以使用了命令 nsenter. 如不想, 可以直接使用
unshare.
2. 目錄 /entry/arch/apps/... 這個是自定義關於容器的目錄, 參見我之前的文章.
3. 用於 unshare namespace 的文件, 需要先行自行建立.
4. 我注意到使用 unshare namespace 操作的那些文件, 比如這裏的 arch.image. 存
在重複掛載的問題, 所以用了個循環來 umount. /nul 這些是 /dev/null 這些文件的符號
鏈接, 可以少寫幾個字.
5. 從自己實際的操作看, 只需要存在有效的 /proc 目錄, 即可完成 pacman 相關操作.
但是因爲沒有做廣泛測試, 所以還是參考 systemd-nspawn, 爲容器添加有效的 /sys 目錄
, 以及 /run 目錄.
6. 處於安全考慮, 同樣參考 systemd-nspawn 把個別系統目錄 ro 掛載. 雖然程序可以改
回 rw.
7. 最後上述提到的系統目錄掛載, 我是放在容器內的腳本. 比如 /proc, 雖然 unshare
有一個參數, 可自動掛載, 但是在 chroot 調用時沒有看到其存在, 可能其不適用於
chroot.
這裏是放在容器內的腳本, 我起名 /usr/local/bin/unshare-shell
#!/bin/bash
set -o nounset
export IFS=$'\n'
declare -r HOST='arch.image'
grep --quiet "${HOST}" /etc/hostname
[ $? -ne 0 ] && exit 1
mount proc /proc --types proc --options=rw,nosuid,nodev,noexec,relatime
# mount sys /sys --types sysfs --options=rw,nosuid,nodev,noexec,relatime
mount tmpfs /run --types tmpfs --options=rw,nosuid,noexec,relatime,size=798576k,mode=755
mount /proc/sys /proc/sys --options=bind,private,ro
mount /proc/sysrq-trigger /proc/sysrq-trigger --options=bind,private,ro
if [[ ! -c /dev/pts/1 || ! -e /dev/pts/1 ]]; then
mkdir --parent /dev/pts
mknod --mode=620 /dev/pts/1 c 136 1
fi
if [[ ! -c /dev/null || ! -e /dev/null ]]; then
[ -f /dev/null ] && rm /dev/null
mknod --mode=666 /dev/null c 1 3
fi
if [[ ! -c /dev/zero || ! -e /dev/zero ]]; then
[ -f /dev/zero ] && rm /dev/zero
mknod --mode=666 /dev/zero c 1 5
fi
ln --symbolic --force /proc/self/fd/0 /dev/stdin
ln --symbolic --force /proc/self/fd/1 /dev/stdout
ln --symbolic --force /proc/self/fd/2 /dev/stderr
unset DISPLAY GTK_IM_MODULE QT_IM_MODULE XAUTHORITY XMODIFIERS
export SUDO_UID=0
export SUDO_GID=0
export SUDO_USER=root
export HOME=/root
/bin/bash
這裏依舊簡單解釋一下:
1. 是容器的 /usr/local/bin, 不是宿主的.
2. mount 用於創建屬於容器的 /proc, 以及對某些系統目錄掛載爲 ro. 個別參數我是硬
編碼. 需要根據自己的環境進行更改. 相關參數來自 findmnt 輸出.
3. 清空不必要的環境變量. 同時修改相關的 UID 和 GID, 我不想在容器內部映射一個和
宿主系統用戶 UID/GID 相同的用戶, 而是直接用 root, 反正只是更新用.
4. 沒有找到很好的, 可指示 bash 交互式 shell, 啓動後自動進入特定目錄的方法. 所以
是在 /root/.bashrc 寫入
cd DIR
3. 仿造宿主系統, 在 /dev 目錄創建基本的若干字符文件. stdin, stdout, stderr,
zero, null. 由於是在容器內, 所以對已存在的文件, 處理比較粗暴.
這個方法在實際使用和 systemd-nspawn 沒有什麼差別.