需求
正常情况下这种操作比较反设计,需要谨慎使用 有些时候我们使用
docker
的时候会需要用到宿主机命令操作,比如- 执行
netplan apply
生效机器网络配置 - 查看宿主机网络信息使用
ifconfig
或者ip addr
- 从容器发出命令重启宿主机
具体实现
docker run
实现 最简单的实现 使用
docker
运行一个ubuntu
容器 进入容器之后执行nsenter
命令查看宿主机网络配置信息$ sudo docker run -it --pid=host --privileged=true ubuntu /bin/bash # 进入容器内部之后执行 /# nsenter -a -t 1 sh -c "ip addr"
$ sudo docker run -it --pid=host --privileged=true ubuntu /bin/bash # 进入容器内部之后执行 /# nsenter -a -t 1 sh -c "ip addr"
docker compose
实现 新建一个
compose.yaml
文件,写入如下内容services: demo: image: python:3-bullseye pid: host privileged: true container_name: demo-exec command: /bin/sh -c "while true; do echo hello; sleep 3600;done"
services: demo: image: python:3-bullseye pid: host privileged: true container_name: demo-exec command: /bin/sh -c "while true; do echo hello; sleep 3600;done"
执行命令
$ docker compose up -d
$ docker compose up -d
进入容器,并且创建写入
demo.py
文件代码$ docker exec -it demo-exec /bin/bash /# cat > demo.py <<EOF import subprocess if __name__ == '__main__': command = 'ls /var/lib/docker' resp = subprocess.run( f'nsenter -m -u -i -n -p -t 1 sh -c "{command}"', capture_output=True, check=True, text=True, shell=True) print(f'stdout: {resp.stdout}') print(f'stderr: {resp.stderr}') EOF
$ docker exec -it demo-exec /bin/bash /# cat > demo.py <<EOF import subprocess if __name__ == '__main__': command = 'ls /var/lib/docker' resp = subprocess.run( f'nsenter -m -u -i -n -p -t 1 sh -c "{command}"', capture_output=True, check=True, text=True, shell=True) print(f'stdout: {resp.stdout}') print(f'stderr: {resp.stderr}') EOF
在容器内部检查文件是否写入成功并且执行程序
/# cat demo.py /# python demo.py
/# cat demo.py /# python demo.py
实现原理
docker
参数 --pid=host
- 使用宿主机命名空间,方便容器获取到宿主机所有进程信息
- 把宿主机的
/proc
文件夹挂载进入容器的/proc
路径,其中/proc/1
作为nsenter
的target
,作为容器向宿主机发送命令的关键部分
--privileged=true
- 使得
docker
容器有root
权限执行宿主机命令,确保从容器执行命令的时候不会产生权限不足错误
nsenter
命令 nsenter
命令是一个可以在指定进程的命令空间下运行指定程序的命令$ nsenter --help 用法: nsenter [选项] [<程序> [<参数>...]] 以其他程序的名字空间运行某个程序。 选项: -a, --all enter all namespaces -t, --target <pid> 要获取名字空间的目标进程 -m, --mount[=<文件>] 进入 mount 名字空间 -u, --uts[=<文件>] 进入 UTS 名字空间(主机名等) -i, --ipc[=<文件>] 进入 System V IPC 名字空间 -n, --net[=<文件>] 进入网络名字空间 -p, --pid[=<文件>] 进入 pid 名字空间 -C, --cgroup[=<文件>] 进入 cgroup 名字空间 -U, --user[=<文件>] 进入用户名字空间 -S, --setuid <uid> 设置进入空间中的 uid -G, --setgid <gid> 设置进入名字空间中的 gid --preserve-credentials 不干涉 uid 或 gid -r, --root[=<目录>] 设置根目录 -w, --wd[=<dir>] 设置工作目录 -F, --no-fork 执行 <程序> 前不 fork -Z, --follow-context 根据 --target PID 设置 SELinux 环境 -h, --help display this help -V, --version display version 更多信息请参阅 nsenter(1)。
$ nsenter --help 用法: nsenter [选项] [<程序> [<参数>...]] 以其他程序的名字空间运行某个程序。 选项: -a, --all enter all namespaces -t, --target <pid> 要获取名字空间的目标进程 -m, --mount[=<文件>] 进入 mount 名字空间 -u, --uts[=<文件>] 进入 UTS 名字空间(主机名等) -i, --ipc[=<文件>] 进入 System V IPC 名字空间 -n, --net[=<文件>] 进入网络名字空间 -p, --pid[=<文件>] 进入 pid 名字空间 -C, --cgroup[=<文件>] 进入 cgroup 名字空间 -U, --user[=<文件>] 进入用户名字空间 -S, --setuid <uid> 设置进入空间中的 uid -G, --setgid <gid> 设置进入名字空间中的 gid --preserve-credentials 不干涉 uid 或 gid -r, --root[=<目录>] 设置根目录 -w, --wd[=<dir>] 设置工作目录 -F, --no-fork 执行 <程序> 前不 fork -Z, --follow-context 根据 --target PID 设置 SELinux 环境 -h, --help display this help -V, --version display version 更多信息请参阅 nsenter(1)。
具体执行
$ nsenter -a -t 1 sh -c "ip addr"
$ nsenter -a -t 1 sh -c "ip addr"
-a
表示进入宿主机的所有命名空间-t 1
表示获取/proc/1
进程,就是pid=1
的进程,这个进程是docker
使用--pid=host
参数挂载进入容器内部的宿主机进程sh -c "ip addr"
就表示发送给宿主机的命令是ip addr
实际使用过程中如果出现宿主机和容器命名空间不一致问题,主要产生原因是宿主机内核版本和容器 所默认的加载内核版本不一致
比如
cgroup
是在Linux4.6
版本加入的,如果使用Ubuntu20
或者其他python3.10
等比较新的镜像启动容器的时候,当nsenter
使用参数-a
,容器会加载所有命名空间,但是cgroup
命名空间在旧版本的系统里面由于内核版本比较旧,所以该命名空间是没有的,最终nsenter
命令就会报错需要按照宿主机有的命名空间来调整
nsenter
参数,可以调整如下$ nsenter -m -u -i -n -p -t 1 sh -c "ip addr"
$ nsenter -m -u -i -n -p -t 1 sh -c "ip addr"
比如可以把
-a
参数替换成-m -u -i -n -p
,明确指定进入mount
,UTS
,System V IPC
,网络
,pid
命名空间 这几个命名空间包含了绝大多数的空间环境,linux
的大部分命令都可以正常执行命名空间说明
namespace
是Linux
中一些进程的属性的作用域,使用命名空间,可以隔离不同的进程Linux
在不断的添加命名空间,目前有mount
:挂载命名空间,使进程有一个独立的挂载文件系统,始于Linux 2.4.19
ipc
:ipc
命名空间,使进程有一个独立的ipc
,包括消息队列,共享内存和信号量,始于Linux 2.6.19
uts
:uts
命名空间,使进程有一个独立的hostname
和domainname
,始于Linux 2.6.19
net
:network
命令空间,使进程有一个独立的网络栈,始于Linux 2.6.24
pid
:pid
命名空间,使进程有一个独立的pid
空间,始于Linux 2.6.24
user
:user
命名空间,是进程有一个独立的user
空间,始于Linux 2.6.23
,结束于Linux 3.8
cgroup
:cgroup
命名空间,使进程有一个独立的cgroup
控制组,始于Linux 4.6
参考阅读
- 执行