Source code for ocrd_network.runtime_data.connection_clients
from __future__ import annotations
from docker import APIClient, DockerClient
from docker.transport import SSHHTTPAdapter
from paramiko import AutoAddPolicy, SSHClient
[docs]
class CustomDockerClient(DockerClient):
"""Wrapper for docker.DockerClient to use an own SshHttpAdapter.
This makes it possible to use provided password/keyfile for connecting with
python-docker-sdk, which otherwise only allows to use ~/.ssh/config for
login
XXX: inspired by https://github.com/docker/docker-py/issues/2416 . Should be replaced when
docker-sdk provides its own way to make it possible to use custom SSH Credentials. Possible
Problems: APIClient must be given the API-version because it cannot connect prior to read it. I
could imagine this could cause Problems. This is not a rushed implementation and was the only
workaround I could find that allows password/keyfile to be used (by default only keyfile from
~/.ssh/config can be used to authenticate via ssh)
XXX 2: Reasons to extend DockerClient: The code-changes regarding the connection should be in
one place, so I decided to create `CustomSshHttpAdapter` as an inner class. The super
constructor *must not* be called to make this workaround work. Otherwise, the APIClient
constructor would be invoked without `version` and that would cause a connection-attempt before
this workaround can be applied.
"""
def __init__(self, user: str, host: str, **kwargs) -> None:
# The super-constructor is not called on purpose. It solely instantiates the APIClient.
# Missing 'version' in that call would raise an error.
# The APIClient is provided here as a replacement for what the super-constructor does
if not (user and host):
raise ValueError("Missing 'user' and 'host' - both must be provided")
if ("password" in kwargs) and ("keypath" in kwargs):
if kwargs["password"] and kwargs["keypath"]:
raise ValueError("Both 'password' and 'keypath' provided - one must be provided")
if ("password" not in kwargs) and ("keypath" not in kwargs):
raise ValueError("Missing 'password' or 'keypath' - one must be provided")
self.api = APIClient(base_url=f"ssh://{host}", use_ssh_client=True, version="1.41")
self.api.mount(
prefix="http+docker://ssh", adapter=self.CustomSshHttpAdapter(base_url=f"ssh://{user}@{host}:22", **kwargs)
)
[docs]
class CustomSshHttpAdapter(SSHHTTPAdapter):
def __init__(self, base_url, password: str = "", keypath: str = "") -> None:
self.password = password
self.keypath = keypath
if not self.password and not self.keypath:
raise Exception("Missing 'password' or 'keypath' - one must be provided")
if self.password and self.keypath:
raise Exception("Both 'password' and 'keypath' provided - one must be provided")
super().__init__(base_url)
def _create_paramiko_client(self, base_url: str) -> None:
"""
this method is called in the superclass constructor. Overwriting allows to set
password/keypath for the internal paramiko-client
"""
super()._create_paramiko_client(base_url)
if self.password:
self.ssh_params["password"] = self.password
elif self.keypath:
self.ssh_params["key_filename"] = self.keypath
self.ssh_client.set_missing_host_key_policy(AutoAddPolicy)
[docs]
def create_docker_client(address: str, username: str, password: str = "", keypath: str = "") -> CustomDockerClient:
return CustomDockerClient(username, address, password=password, keypath=keypath)
[docs]
def create_ssh_client(address: str, username: str, password: str = "", keypath: str = "") -> SSHClient:
client = SSHClient()
client.set_missing_host_key_policy(AutoAddPolicy)
try:
client.connect(hostname=address, username=username, password=password, key_filename=keypath)
except Exception as error:
raise Exception(f"Error creating SSHClient of host '{address}', reason: {error}") from error
return client