繁体   English   中英

如何从 docker 容器在主机上运行 shell 脚本?

[英]How to run shell script on host from docker container?

如何从 docker 容器控制主机?

例如,如何执行复制到主机 bash 脚本?

使用命名管道。 在主机操作系统上,创建一个脚本来循环和读取命令,然后调用eval

让 docker 容器读取到该命名管道。

为了能够访问管道,您需要通过卷安装它。

这类似于 SSH 机制(或类似的基于套接字的方法),但将您正确地限制在主机设备上,这可能更好。 另外,您不必传递身份验证信息。

我唯一的警告是要小心你为什么这样做。 如果您想创建一种使用用户输入或其他方式进行自我升级的方法,这完全是一件事情,但您可能不想调用命令来获取一些配置数据,因为正确的方法是将其传递为args/volume 进入 docker。 另外,请注意您正在评估的事实,因此请考虑一下权限模型。

其他一些答案,例如运行脚本。 由于它们无法访问完整的系统资源,因此通常无法在卷下工作,但根据您的使用情况,它可能更合适。

这个答案只是Bradford Medeiros 解决方案的更详细版本,对我来说这也是最好的答案,所以归功于他。

在他的回答中,他解释了要做什么(命名管道),但不完全解释如何去做。

我不得不承认,当我阅读他的解决方案时,我不知道什么是命名管道。 所以我努力实现它(虽然它实际上很简单),但我确实成功了。 所以我回答的重点只是详细说明您需要运行的命令才能使其正常工作,但再次感谢他。

第 1 部分 - 在没有 docker 的情况下测试命名管道概念

在主主机上,选择要放置命名管道文件的文件夹,例如/path/to/pipe/和管道名称,例如mypipe ,然后运行:

mkfifo /path/to/pipe/mypipe

管道已创建。 类型

ls -l /path/to/pipe/mypipe 

并检查以“p”开头的访问权限,如

prw-r--r-- 1 root root 0 mypipe

现在运行:

tail -f /path/to/pipe/mypipe

终端现在正在等待将数据发送到此管道

现在打开另一个终端窗口。

然后运行:

echo "hello world" > /path/to/pipe/mypipe

检查第一个终端(带有tail -f的终端),它应该显示“hello world”

第 2 部分 - 通过管道运行命令

在主机容器上,不要运行仅输出作为输入发送的任何内容的tail -f ,而是运行以下命令,将其作为命令执行:

eval "$(cat /path/to/pipe/mypipe)"

然后,从另一个终端尝试运行:

echo "ls -l" > /path/to/pipe/mypipe

返回第一个终端,您应该会看到ls -l命令的结果。

第 3 部分 - 让它永远聆听

您可能已经注意到,在上一部分中,在显示ls -l输出之后,它会停止侦听命令。

而不是eval "$(cat /path/to/pipe/mypipe)" ,运行:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done

(你可以不知道)

现在您可以一个接一个地发送无限数量的命令,它们都将被执行,而不仅仅是第一个。

第 4 部分 - 即使在重新启动时也能正常工作

唯一需要注意的是,如果主机必须重新启动,“while”循环将停止工作。

要处理重新启动,这里我做了什么:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done while true; do eval "$(cat /path/to/pipe/mypipe)"; done 在带有#!/bin/bash标头的名为execpipe.sh的文件中while true; do eval "$(cat /path/to/pipe/mypipe)"; done

不要忘记chmod +x

通过运行将其添加到 crontab

crontab -e

然后添加

@reboot /path/to/execpipe.sh

在这一点上,测试它:重启你的服务器,当它备份时,将一些命令回显到管道中并检查它们是否被执行。 当然,您无法看到命令的输出,因此ls -l无济于事,但touch somefile会有所帮助。

另一种选择是修改脚本以将输出放入文件中,例如:

while true; do eval "$(cat /path/to/pipe/mypipe)" &> /somepath/output.txt; done

现在您可以运行ls -l并且输出(在 bash 中使用&>的 stdout 和 stderr)应该在 output.txt 中。

第 5 部分 - 使其与 docker 一起工作

如果您像我一样同时使用 docker compose 和 dockerfile,这就是我所做的:

假设您要将 mypipe 的父文件夹挂载为容器中的/hostpipe

添加这个:

VOLUME /hostpipe

在您的 dockerfile 中以创建挂载点

然后添加这个:

volumes:
   - /path/to/pipe:/hostpipe

在您的 docker compose 文件中,以便将 /path/to/pipe 挂载为 /hostpipe

重新启动 docker 容器。

第 6 部分 - 测试

执行到您的 docker 容器中:

docker exec -it <container> bash

进入挂载文件夹并检查您是否可以看到管道:

cd /hostpipe && ls -l

现在尝试从容器中运行命令:

echo "touch this_file_was_created_on_main_host_from_a_container.txt" > /hostpipe/mypipe

它应该工作!

警告:如果你有一个 OSX (Mac OS) 主机和一个 Linux 容器,它不会工作(解释在这里https://stackoverflow.com/a/43474708/10018801并在这里发布https://github.com/docker /for-mac/issues/483 )因为管道实现不一样,所以你从Linux写入管道的内容只能由Linux读取,而你从Mac OS写入管道的内容只能由a读取Mac OS(这句话可能不太准确,但请注意存在跨平台问题)。

例如,当我从我的 Mac OS 计算机在 DEV 中运行我的 docker setup 时,如上所述的命名管道不起作用。 但在登台和生产中,我有 Linux 主机和 Linux 容器,而且它运行良好。

第 7 部分 - Node.JS 容器中的示例

以下是我如何从 Node.JS 容器向主主机发送命令并检索输出:

const pipePath = "/hostpipe/mypipe"
const outputPath = "/hostpipe/output.txt"
const commandToRun = "pwd && ls-l"

console.log("delete previous output")
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)

console.log("writing to pipe...")
const wstream = fs.createWriteStream(pipePath)
wstream.write(commandToRun)
wstream.close()

console.log("waiting for output.txt...") //there are better ways to do that than setInterval
let timeout = 10000 //stop waiting after 10 seconds (something might be wrong)
const timeoutStart = Date.now()
const myLoop = setInterval(function () {
    if (Date.now() - timeoutStart > timeout) {
        clearInterval(myLoop);
        console.log("timed out")
    } else {
        //if output.txt exists, read it
        if (fs.existsSync(outputPath)) {
            clearInterval(myLoop);
            const data = fs.readFileSync(outputPath).toString()
            if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath) //delete the output file
            console.log(data) //log the output of the command
        }
    }
}, 300);

我使用的解决方案是通过SSH连接到主机并执行如下命令:

ssh -l ${USERNAME} ${HOSTNAME} "${SCRIPT}"

更新

由于这个答案不断获得投票,我想提醒(并强烈推荐),用于调用脚本的帐户应该是一个完全没有权限的帐户,但只能将该脚本作为sudo执行(可以从sudoers文件中完成)。

更新:命名管道

我上面建议的解决方案只是我对 Docker 比较陌生时使用的解决方案。 现在在 2021 年,看看有关命名管道的答案。 这似乎是一个更好的解决方案。

但是,那里没有人提到安全性 将评估通过管道发送的命令的脚本(调用eval的脚本)实际上不能eval用于整个管道输出,而是处理特定情况并根据发送的文本调用所需的命令,否则任何可以做任何事情都可以通过管道发送。

这真的取决于你需要那个 bash 脚本来做什么!

例如,如果 bash 脚本只是回显一些输出,你可以这样做

docker run --rm -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

另一种可能性是您希望 bash 脚本安装一些软件——比如安装 docker-compose 的脚本。 你可以做类似的事情

docker run --rm -v /usr/bin:/usr/bin --privileged -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

但是在这一点上,您真的必须深入了解脚本正在做什么以允许从容器内部对您的主机所需的特定权限。

我的懒惰使我找到了未在此处作为答案发布的最简单的解决方案。

它基于luc juggery精彩文章

为了从 docker 容器中获得 linux 主机的完整 shell,您需要做的就是:

docker run --privileged --pid=host -it alpine:3.8 \
nsenter -t 1 -m -u -n -i sh

解释:

--privileged :授予容器额外的权限,它允许容器访问主机的设备(/dev)

--pid=host :允许容器使用 Docker 主机(运行 Docker 守护进程的虚拟机)的进程树 nsenter 实用程序:允许在现有命名空间(为容器提供隔离的构建块)中运行进程

nsenter (-t 1 -m -u -n -i sh) 允许在与 PID 1 的进程相同的隔离上下文中运行进程 sh。然后整个命令将在 VM 中提供交互式 sh shell

此设置具有重大安全隐患,应谨慎使用(如果有)。

编写一个简单的服务器 python 服务器侦听端口(比如 8080),将端口 -p 8080:8080 与容器绑定,向 localhost:8080 发出 HTTP 请求以询问 python 服务器运行带有 popen 的 shell 脚本,运行 curl 或编写代码以发出 HTTP 请求 curl -d '{"foo":"bar"}' localhost:8080

#!/usr/bin/python
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import subprocess
import json

PORT_NUMBER = 8080

# This class will handles any incoming request from
# the browser 
class myHandler(BaseHTTPRequestHandler):
        def do_POST(self):
                content_len = int(self.headers.getheader('content-length'))
                post_body = self.rfile.read(content_len)
                self.send_response(200)
                self.end_headers()
                data = json.loads(post_body)

                # Use the post data
                cmd = "your shell cmd"
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
                p_status = p.wait()
                (output, err) = p.communicate()
                print "Command output : ", output
                print "Command exit status/return code : ", p_status

                self.wfile.write(cmd + "\n")
                return
try:
        # Create a web server and define the handler to manage the
        # incoming request
        server = HTTPServer(('', PORT_NUMBER), myHandler)
        print 'Started httpserver on port ' , PORT_NUMBER

        # Wait forever for incoming http requests
        server.serve_forever()

except KeyboardInterrupt:
        print '^C received, shutting down the web server'
        server.socket.close()

如果您不担心安全性并且您只是想从另一个 docker 容器(如 OP)中启动主机上的 docker 容器,您可以通过共享它的侦听套接字将主机上运行的 docker 服务器与 docker 容器共享。

请参阅https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface并查看您的个人风险承受能力是否允许此特定应用程序。

您可以通过将以下卷参数添加到您的启动命令来做到这一点

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

或通过在您的 docker compose 文件中共享 /var/run/docker.sock ,如下所示:

version: '3'

services:
   ci:
      command: ...
      image: ...
      volumes:
         - /var/run/docker.sock:/var/run/docker.sock

当您在 docker 容器中运行 docker start 命令时,在您的主机上运行的 docker 服务器将看到请求并配置同级容器。

信用:http: //jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/

docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
# 

我有一个简单的方法。

第 1 步:挂载 /var/run/docker.sock:/var/run/docker.sock (这样您就可以在容器内执行 docker 命令)

第 2 步:在您的容器中执行以下操作。 这里的关键部分是( --network host因为这将从主机上下文执行)

docker run -i --rm --network host -v /opt/test.sh:/test.sh alpine:3.7 sh /test.sh

test.sh 应该包含一些你需要的命令(ifconfig、netstat 等)。 现在您将能够获得主机上下文输出。

正如 Marcus 所提醒的,docker 基本上是进程隔离。 从 docker 1.8 开始,您可以在主机和容器之间双向复制文件,请参阅docker cp的文档

https://docs.docker.com/reference/commandline/cp/

复制文件后,您可以在本地运行它

您可以使用管道概念,但使用主机上的文件和fswatch来完成从 docker 容器在主机上执行脚本的目标。 像这样(使用风险自负):

#! /bin/bash

touch .command_pipe
chmod +x .command_pipe

# Use fswatch to execute a command on the host machine and log result
fswatch -o --event Updated .command_pipe | \
            xargs -n1 -I "{}"  .command_pipe >> .command_pipe_log  &

 docker run -it --rm  \
   --name alpine  \
   -w /home/test \
   -v $PWD/.command_pipe:/dev/command_pipe \
   alpine:3.7 sh

rm -rf .command_pipe
kill %1

在此示例中,在容器内将命令发送到 /dev/command_pipe,如下所示:

/home/test # echo 'docker network create test2.network.com' > /dev/command_pipe

在主机上,您可以检查网络是否已创建:

$ docker network ls | grep test2
8e029ec83afe        test2.network.com                            bridge              local

在我的场景中,我只是在容器中 ssh 登录主机(通过主机 ip),然后我可以对主机做任何我想做的事情

我发现使用命名管道的答案很棒。 但我想知道是否有办法获得执行命令的 output。

解决方案是创建两个命名管道:

mkfifo /path/to/pipe/exec_in
mkfifo /path/to/pipe/exec_out

然后,@Vincent 建议的使用循环的解决方案将变为:

# on the host
while true; do eval "$(cat exec_in)" > exec_out; done

然后在 docker 容器上,我们可以执行命令并使用以下命令获取 output:

# on the container
echo "ls -l" > /path/to/pipe/exec_in
cat /path/to/pipe/exec_out

如果有人感兴趣,我需要在容器上的主机上使用故障转移 IP,我创建了这个简单的 ruby 方法:

def fifo_exec(cmd)
  exec_in = '/path/to/pipe/exec_in'
  exec_out = '/path/to/pipe/exec_out'
  %x[ echo #{cmd} > #{exec_in} ]
  %x[ cat #{exec_out} ]
end
# example
fifo_exec "curl https://ip4.seeip.org"

要扩展user2915097 的响应

隔离的想法是能够非常清楚地限制应用程序/进程/容器(无论您的角度是什么)可以对主机系统执行的操作。 因此,能够复制和执行文件将真正打破整个概念。

是的。 但有时它是必要的。

不,事实并非如此,或者 Docker 不是正确使用的东西。 您应该做的是为您想要做的事情声明一个清晰的接口(例如更新主机配置),并编写一个最小的客户端/服务器来完全做到这一点,仅此而已。 然而,一般来说,这似乎不是很理想。 在许多情况下,您应该简单地重新考虑您的方法并消除这种需求。 当基本上所有东西都是可以使用某种协议访问的服务时,Docker 就应运而生了。 我想不出任何合适的 Docker 容器获得在主机上执行任意内容的权利的用例。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM