• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Python使用SSH代理访问远程Docker

武飞扬头像
shirukai
帮助1

Python使用SSH代理访问远程Docker

Docker 20.10.17

Python 2.7

1 前言

使用过docker-py客户端的同学,肯定都知道,创建client实例的时候,需要在构造函数中传入base_url这个参数,或者指定指定环境变量DOCKER_HOST,例如:

import docker
client = docker.DockerClient(base_url='unix://var/run/docker.sock')

其中unix协议表示使用的是本地的UNIX Domain Socket,这是Docker用于同一台主机的进程通讯,顾名思义要想使用这种方式客户端需要跟Docker进程在同一台主机上。当然docker-py还支持别的协议,通过源码注释我们可以看到它还支持tcp的方式,例如:

import docker
client = docker.DockerClient(base_url='tcp://127.0.0.1:1234')

看到这里,上面的问题通过tcp访问远程的Docker服务不就可以了吗,但是这里需要注意的是,默认的Docker服务是不会启用TCP的监听端口的,需要在启动服务式做一些改造,修改启动脚本/etc/systemd/system/docker.service.d/tcp.conf

ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375

这种方式需要修改Docker服务,而且暴露端口可能会引起一些安全问题,我们的生产环境中通常都是使用的默认的UNIX Domain Socket的方式,显然TCP的方式不适合我们,那还有什么方式呢?继续查看源码,我们看到,原来docker-py是支持ssh的,例如:

import docker
client = docker.DockerClient(base_url='ssh:/root@127.0.0.1:22')

当时我看到这里仿佛看到了新大路,之前的问题迎刃而解,但通过实验发现行不通

学新通

这需要docker-py客户端所在的机器要与Docker服务的机器进行免密登录,设置互信,显然这种方式也不是很合适。

2 通过改造SSH用户密码认证访问远程Docker

这种方式自己实现的代码逻辑比较简单,两个关键类需要重写SSHHTTPAdapter和DockerClient。思路是:

  1. 解析base_url中的用户密码信息
  2. 通过用户名密码创建SSH连接
  3. 让DockerClient使用我们创建的SSHHTTPAdapter

2.1 解析base_url

定义一个函数用来解析base_url,例如:ssh://Username:Password@127.0.0.1:22

def docker_urlparse(url):
    # fix for url with '#'
    mark = '_a5s7m3_'
    r = urlparse.urlparse(url.replace('#', mark))
    hostname = r.hostname.replace(mark, '#')
    username = r.username.replace(mark, '#')
    password = r.password.replace(mark, '#')
    port = r.port
    scheme = r.scheme
    return {
        'hostname': hostname,
        'port': port,
        'username': username,
        'password': password,
        'scheme': scheme
    }
学新通

2.2 重写SSHHTTPAdapter

重写SSHHTTPAdapter的目的是能够通过用户名密码创建连接,主要是重写方法_create_paramiko_client

class SSHHTTPAdapter(transport.SSHHTTPAdapter):

    def __init__(self, ssh_params, timeout=60, pool_connections=DEFAULT_NUM_POOLS,
                 max_pool_size=DEFAULT_MAX_POOL_SIZE, shell_out=False):
        self.ssh_params = ssh_params
        del ssh_params['scheme']
        super(SSHHTTPAdapter, self).__init__('', timeout, pool_connections, max_pool_size, shell_out)

    def _create_paramiko_client(self, _):
        logging.getLogger("paramiko").setLevel(logging.WARNING)
        self.ssh_client = paramiko.SSHClient()
        ssh_config_file = os.path.expanduser("~/.ssh/config")
        if os.path.exists(ssh_config_file):
            conf = paramiko.SSHConfig()
            with open(ssh_config_file) as f:
                conf.parse(f)
            host_config = conf.lookup(self.ssh_params['hostname'])
            self.ssh_conf = host_config
            if 'proxycommand' in host_config:
                self.ssh_params["sock"] = paramiko.ProxyCommand(
                    self.ssh_conf['proxycommand']
                )
            if 'hostname' in host_config:
                self.ssh_params['hostname'] = host_config['hostname']
            if self.ssh_params['port'] is None and 'port' in host_config:
                self.ssh_params['port'] = self.ssh_conf['port']
            if self.ssh_params['username'] is None and 'user' in host_config:
                self.ssh_params['username'] = self.ssh_conf['user']

        self.ssh_client.load_system_host_keys()
        self.ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy())
学新通

2.3 重写DockerClient

使用自定义的SSHHTTPAdapter创建对应的adapter实例,然后绑定到DockerClient的api上,具体实现如下:

class SSHDockerClient(DockerClient):

    def __init__(self, *args, **kwargs):
        base_url = kwargs.get('base_url')
        ssh_params = docker_urlparse(base_url)
        adapter = SSHHTTPAdapter(ssh_params)
        kwargs['base_url'] = ''
        kwargs['version'] = MINIMUM_DOCKER_API_VERSION
        super(SSHDockerClient, self).__init__(*args, **kwargs)
        self.api.mount('http docker://ssh', adapter)
        self.api.base_url = 'http docker://ssh'

2.4 验证测试

base_url格式:ssh://<用户名>:<密码>@<主机>:<端口>

client = SSHDockerClient(base_url='ssh://docker:xxxxx@127.0.0.1:22')
client.version()

3 通过SSH命令行隧道方式访问远程Docker

网上还有一些资料,是使用SSH命令随带的方式代理远程Docker的Unix Domain Socket,我一开始也是用的这种方式。

3.1 命令行使用

命令行执行SSH命令创建隧道

ssh -nNT -L /tmp/docker.sock:/var/run/docker.sock root@127.0.0.1

然后客户端直接这样用

import docker
client = docker.DockerClient(base_url='unix:///tmp/docker.sock')

3.2 使用代码封装命令

上面需要单独执行命令行,不符合需求,我们可以将命令用代码封装起来,在Python中使用subprocess单独启动一个命令行进程,然后再创建对应的客户端实例。思路很简单:

  1. 使用subprocess单独运行ssh命令
  2. 等待本地的/tmp/docker.sock可用
  3. 使用/tmp/docker.sock创建客户端实例

具体代码实现:

class ProxyDockerClient(DockerClient):
    remote_sock = '/var/run/docker.sock'
    local_socks_dir = '/tmp/docker-client-proxy'
    proxy_process = None  # type: subprocess

    def __init__(self, *args, **kwargs):
        base_url = kwargs.get('base_url')
        ssh_params = docker_urlparse(base_url)
        kwargs['base_url'] = self.ssh_proxy(ssh_params)

        super(ProxyDockerClient, self).__init__(*args, **kwargs)

    def ssh_proxy(self, ssh_params):
        if ssh_params['scheme'] == 'ssh':
            local_sock = os.path.join(self.local_socks_dir, ssh_params['hostname'], os.path.basename(self.remote_sock))
            local_sock_dir = os.path.dirname(local_sock)
            shutil.rmtree(local_sock_dir, ignore_errors=True)
            os.makedirs(local_sock_dir)
            base_url = 'unix://%s' % local_sock
            self.proxy_process = subprocess.Popen(
                args=['sshpass', '-p', ssh_params['password'], 'ssh', '-nNT', '-L',
                      '%s:%s' % (local_sock, self.remote_sock),
                      '%s@%s' % (ssh_params['username'], ssh_params['hostname'])],
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                close_fds=True)
            connected = self._wait_connectable(local_sock)

            if not connected:
                self.proxy_process.kill()
                raise RuntimeError("Can't connected!")
            return base_url

    @staticmethod
    def _wait_connectable(sock_address, retries=20):
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        connected = False
        retry_times = 0
        while not connected and retry_times < retries:
            try:
                sock.connect(sock_address)
                connected = True
            except socket.error, msg:
                logging.error(msg)
            finally:
                retry_times  = 1
                time.sleep(0.5)
        return connected
学新通

3.3 验证测试

base_url格式:ssh://<用户名>:<密码>@<主机>:<端口>

client = ProxyDockerClient(base_url='ssh://docker:xxxxx@127.0.0.1:22')
client.version()

注意:这种方式在CentOS下需要使用非root用户,并且将/var/run/docker.sock分配非root权限,否则会报错。

centos不能使用root用户 https://bugzilla.redhat.com/show_bug.cgi?id=1527565
https://rancher.com/docs/rke/latest/en/troubleshooting/ssh-connectivity-errors/

学新通

4 总结

对比两种方案的实现,建议使用第一种,第二种是我最早一版本的实现,当时测试机器是Ubuntu,并没有发现问问题,后来切换到CentOS出现问题了,排查了很久才找到原因。关于**如何使用Python操作其它机器上远程的Docker服务?**这个问题的解决方案就总结这么多,同学们有什么更好的方案可以一块交流学习。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhggeiff
系列文章
更多 icon
同类精品
更多 icon
继续加载