Source code for ocrd_network.deployment_utils

from __future__ import annotations
from enum import Enum
from docker import APIClient, DockerClient
from docker.transport import SSHHTTPAdapter
from paramiko import AutoAddPolicy, SSHClient
from time import sleep
import re

from .rabbitmq_utils import RMQPublisher
from pymongo import MongoClient

__all__ = [
    'create_docker_client',
    'create_ssh_client',
    'DeployType',
    'verify_mongodb_available',
    'verify_rabbitmq_available'
]


[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:") from error return client
[docs]def create_docker_client(address: str, username: str, password: str = "", keypath: str = "") -> CustomDockerClient: return CustomDockerClient(username, address, password=password, keypath=keypath)
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. The # missing `version` in that call would raise an error. APIClient is provided here as a # replacement for what the super-constructor does if not (user and host): raise ValueError('Missing argument: user and host must both be provided') if ('password' not in kwargs) != ('keypath' not in kwargs): raise ValueError('Missing argument: one of password and keyfile is needed') self.api = APIClient(f'ssh://{host}', use_ssh_client=True, version='1.41') ssh_adapter = self.CustomSshHttpAdapter(f'ssh://{user}@{host}:22', **kwargs) self.api.mount('http+docker://ssh', ssh_adapter) class CustomSshHttpAdapter(SSHHTTPAdapter): def __init__(self, base_url, password: str = "", keypath: str = "") -> None: self.password = password self.keypath = keypath if bool(self.password) == bool(self.keypath): raise Exception("Either 'password' or 'keypath' 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 verify_rabbitmq_available( host: str, port: int, vhost: str, username: str, password: str ) -> None: max_waiting_steps = 15 while max_waiting_steps > 0: try: dummy_publisher = RMQPublisher(host=host, port=port, vhost=vhost) dummy_publisher.authenticate_and_connect(username=username, password=password) except Exception: max_waiting_steps -= 1 sleep(2) else: # TODO: Disconnect the dummy_publisher here before returning... return raise RuntimeError(f'Cannot connect to RabbitMQ host: {host}, port: {port}, ' f'vhost: {vhost}, username: {username}')
[docs]def verify_mongodb_available(mongo_url: str) -> None: try: client = MongoClient(mongo_url, serverSelectionTimeoutMS=1000.0) client.admin.command("ismaster") except Exception: raise RuntimeError(f'Cannot connect to MongoDB: {re.sub(r":[^@]+@", ":****@", mongo_url)}')
[docs]class DeployType(Enum): """ Deploy-Type of the processing worker/processor server. """ DOCKER = 1 NATIVE = 2