diff options
Diffstat (limited to 'oti/event-handler/otihandler/docker_client.py')
-rw-r--r-- | oti/event-handler/otihandler/docker_client.py | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/oti/event-handler/otihandler/docker_client.py b/oti/event-handler/otihandler/docker_client.py new file mode 100644 index 0000000..621a1ec --- /dev/null +++ b/oti/event-handler/otihandler/docker_client.py @@ -0,0 +1,175 @@ +# ================================================================================ +# Copyright (c) 2019-2020 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= + +"""client interface to docker""" + +import docker +import json +import logging +import time + +from otihandler.config import Config +from otihandler.consul_client import ConsulClient +from otihandler.utils import decrypt + + +# class DockerClientError(RuntimeError): +# pass + +class DockerClientConnectionError(RuntimeError): + pass + + +class DockerClient(object): + """ + All Docker logins are in Consul's key-value store under + "docker_plugin/docker_logins" as a list of json objects where + each object is a single login: + + [{ "username": "XXXX", "password": "yyyy", + "registry": "hostname.domain:18443" }] + """ + + _logger = logging.getLogger("oti_handler.docker_client") + + def __init__(self, docker_host, reauth=False): + """Create Docker client + + Args: + ----- + reauth: (boolean) Forces reauthentication, e.g., Docker login + """ + + (fqdn, port) = ConsulClient.get_service_fqdn_port(docker_host, node_meta=True) + base_url = "https://{}:{}".format(fqdn, port) + + try: + tls_config = docker.tls.TLSConfig( + client_cert=( + Config.tls_server_ca_chain_file, + Config.tls_private_key_file + ) + ) + self._client = docker.APIClient(base_url=base_url, tls=tls_config, version='auto', timeout=60) + + for dcl in ConsulClient.get_value("docker_plugin/docker_logins"): + dcl['password'] = decrypt(dcl['password']) + dcl["reauth"] = reauth + self._client.login(**dcl) + + # except requests.exceptions.RequestException as e: + except Exception as e: + msg = "DockerClient.__init__({}) attempt to {} with TLS got exception {}: {!s}".format( + docker_host, base_url, type(e).__name__, e) + + # Then try connecting to dockerhost without TLS + try: + base_url = "tcp://{}:{}".format(fqdn, port) + self._client = docker.APIClient(base_url=base_url, tls=False, version='auto', timeout=60) + + for dcl in ConsulClient.get_value("docker_plugin/docker_logins"): + dcl['password'] = decrypt(dcl['password']) + dcl["reauth"] = reauth + self._client.login(**dcl) + + # except requests.exceptions.RequestException as e: + except Exception as e: + msg = "{}\nDockerClient.__init__({}) attempt to {} without TLS got exception {}: {!s}".format( + msg, docker_host, base_url, type(e).__name__, e) + DockerClient._logger.error(msg) + raise DockerClientConnectionError(msg) + + @staticmethod + def build_cmd(script_path, use_sh=True, msg_type="dti", **kwargs): + """Build command to execute""" + + data = json.dumps(kwargs or {}) + + if use_sh: + return ['/bin/sh', script_path, msg_type, data] + else: + return [script_path, msg_type, data] + + def notify_for_reconfiguration(self, container_id, cmd): + """Notify Docker container that reconfiguration occurred + + Notify the Docker container by doing Docker exec of passed-in command + + Args: + ----- + container_id: (string) + cmd: (list) of strings each entry being part of the command + """ + + for attempts_remaining in range(11,-1,-1): + try: + result = self._client.exec_create(container=container_id, cmd=cmd) + except docker.errors.APIError as e: + # e # 500 Server Error: Internal Server Error ("{"message":"Container 624108d1ab96f24b568662ca0e5ffc39b59c1c57431aec0bef231fb62b04e166 is not running"}") + DockerClient._logger.debug("exec_create() returned APIError: {!s}".format(e)) + + # e.message # 500 Server Error: Internal Server Error + # DockerClient._logger.debug("e.message: {}".format(e.message)) + # e.response.status_code # 500 + # DockerClient._logger.debug("e.response.status_code: {}".format(e.response.status_code)) + # e.response.reason # Internal Server Error + # DockerClient._logger.debug("e.response.reason: {}".format(e.response.reason)) + # e.explanation # {"message":"Container 624108d1ab96f24b568662ca0e5ffc39b59c1c57431aec0bef231fb62b04e166 is not running"} + # DockerClient._logger.debug("e.explanation: {}".format(e.explanation)) + + # docker container restarting can wait + if e.explanation and 'is restarting' in e.explanation.lower(): + DockerClient._logger.debug("notification exec_create() experienced: {!s}".format(e)) + if attempts_remaining == 0: + result = None + break + time.sleep(10) + # elif e.explanation and 'no such container' in e.explanation.lower(): + # elif e.explanation and 'is not running' in e.explanation.lower(): + else: + DockerClient._logger.warn("aborting notification exec_create() because exception {}: {!s}".format(type(e).__name__, e)) + return str(e) # don't raise or CM will retry usually forever + # raise DockerClientError(e) + except Exception as e: + DockerClient._logger.warn("aborting notification exec_create() because exception {}: {!s}".format( + type(e).__name__, e)) + return str(e) # don't raise or CM will retry usually forever + # raise DockerClientError(e) + else: + break + if not result: + DockerClient._logger.warn("aborting notification exec_create() because docker exec failed") + return "notification unsuccessful" # failed to get an exec_id, perhaps trying multiple times, so don't raise or CM will retry usually forever + DockerClient._logger.debug("notification exec_create() succeeded") + + for attempts_remaining in range(11,-1,-1): + try: + result = self._client.exec_start(exec_id=result['Id']) + except Exception as e: + DockerClient._logger.debug("notification exec_start() got exception {}: {!s}".format(type(e).__name__, e)) + if attempts_remaining == 0: + DockerClient._logger.warn("aborting notification exec_start() because exception {}: {!s}".format(type(e).__name__, e)) + return str(e) # don't raise or CM will retry usually forever + # raise DockerClientError(e) + time.sleep(10) + else: + break + DockerClient._logger.debug("notification exec_start() succeeded") + + DockerClient._logger.info("Pass to docker exec {} {} {}".format( + container_id, cmd, result)) + + return result |