Merge "Initial delivery of helm charts to deploy mod2 components. Resolved all the...
[dcaegen2/platform.git] / oti / event-handler / otihandler / docker_client.py
1 # ================================================================================
2 # Copyright (c) 2019-2020 AT&T Intellectual Property. All rights reserved.
3 # ================================================================================
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #      http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 # ============LICENSE_END=========================================================
16
17 """client interface to docker"""
18
19 import docker
20 import json
21 import logging
22 import time
23
24 from otihandler.config import Config
25 from otihandler.consul_client import ConsulClient
26 from otihandler.utils import decrypt
27
28
29 # class DockerClientError(RuntimeError):
30 #     pass
31
32 class DockerClientConnectionError(RuntimeError):
33     pass
34
35
36 class DockerClient(object):
37     """
38     All Docker logins are in Consul's key-value store under
39     "docker_plugin/docker_logins" as a list of json objects where
40     each object is a single login:
41
42         [{ "username": "XXXX", "password": "yyyy",
43            "registry": "hostname.domain:18443" }]
44     """
45
46     _logger = logging.getLogger("oti_handler.docker_client")
47
48     def __init__(self, docker_host, reauth=False):
49         """Create Docker client
50
51         Args:
52         -----
53         reauth: (boolean) Forces reauthentication, e.g., Docker login
54         """
55
56         (fqdn, port) = ConsulClient.get_service_fqdn_port(docker_host, node_meta=True)
57         base_url = "https://{}:{}".format(fqdn, port)
58
59         try:
60             tls_config = docker.tls.TLSConfig(
61                 client_cert=(
62                     Config.tls_server_ca_chain_file,
63                     Config.tls_private_key_file
64                 )
65             )
66             self._client = docker.APIClient(base_url=base_url, tls=tls_config, version='auto', timeout=60)
67
68             for dcl in ConsulClient.get_value("docker_plugin/docker_logins"):
69                 dcl['password'] = decrypt(dcl['password'])
70                 dcl["reauth"] = reauth
71                 self._client.login(**dcl)
72
73         # except requests.exceptions.RequestException as e:
74         except Exception as e:
75             msg = "DockerClient.__init__({}) attempt to {} with TLS got exception {}: {!s}".format(
76                       docker_host, base_url, type(e).__name__, e)
77
78             # Then try connecting to dockerhost without TLS
79             try:
80                 base_url = "tcp://{}:{}".format(fqdn, port)
81                 self._client = docker.APIClient(base_url=base_url, tls=False, version='auto', timeout=60)
82
83                 for dcl in ConsulClient.get_value("docker_plugin/docker_logins"):
84                     dcl['password'] = decrypt(dcl['password'])
85                     dcl["reauth"] = reauth
86                     self._client.login(**dcl)
87
88             # except requests.exceptions.RequestException as e:
89             except Exception as e:
90                 msg = "{}\nDockerClient.__init__({}) attempt to {} without TLS got exception {}: {!s}".format(
91                           msg, docker_host, base_url, type(e).__name__, e)
92                 DockerClient._logger.error(msg)
93                 raise DockerClientConnectionError(msg)
94
95     @staticmethod
96     def build_cmd(script_path, use_sh=True, msg_type="dti", **kwargs):
97         """Build command to execute"""
98
99         data = json.dumps(kwargs or {})
100
101         if use_sh:
102             return ['/bin/sh', script_path, msg_type, data]
103         else:
104             return [script_path, msg_type, data]
105
106     def notify_for_reconfiguration(self, container_id, cmd):
107         """Notify Docker container that reconfiguration occurred
108
109         Notify the Docker container by doing Docker exec of passed-in command
110
111         Args:
112         -----
113         container_id: (string)
114         cmd: (list) of strings each entry being part of the command
115         """
116
117         for attempts_remaining in range(11,-1,-1):
118             try:
119                 result = self._client.exec_create(container=container_id, cmd=cmd)
120             except docker.errors.APIError as e:
121                 # e  #  500 Server Error: Internal Server Error ("{"message":"Container 624108d1ab96f24b568662ca0e5ffc39b59c1c57431aec0bef231fb62b04e166 is not running"}")
122                 DockerClient._logger.debug("exec_create() returned APIError: {!s}".format(e))
123
124                 # e.message               # 500 Server Error: Internal Server Error
125                 # DockerClient._logger.debug("e.message: {}".format(e.message))
126                 # e.response.status_code  # 500
127                 # DockerClient._logger.debug("e.response.status_code: {}".format(e.response.status_code))
128                 # e.response.reason       # Internal Server Error
129                 # DockerClient._logger.debug("e.response.reason: {}".format(e.response.reason))
130                 # e.explanation           # {"message":"Container 624108d1ab96f24b568662ca0e5ffc39b59c1c57431aec0bef231fb62b04e166 is not running"}
131                 # DockerClient._logger.debug("e.explanation: {}".format(e.explanation))
132
133                 # docker container restarting can wait
134                 if e.explanation and 'is restarting' in e.explanation.lower():
135                     DockerClient._logger.debug("notification exec_create() experienced: {!s}".format(e))
136                     if attempts_remaining == 0:
137                         result = None
138                         break
139                     time.sleep(10)
140                 # elif e.explanation and 'no such container' in e.explanation.lower():
141                 # elif e.explanation and 'is not running' in e.explanation.lower():
142                 else:
143                     DockerClient._logger.warn("aborting notification exec_create() because exception {}: {!s}".format(type(e).__name__, e))
144                     return str(e)  # don't raise or CM will retry usually forever
145                     # raise DockerClientError(e)
146             except Exception as e:
147                 DockerClient._logger.warn("aborting notification exec_create() because exception {}: {!s}".format(
148                     type(e).__name__, e))
149                 return str(e)  # don't raise or CM will retry usually forever
150                 # raise DockerClientError(e)
151             else:
152                 break
153         if not result:
154             DockerClient._logger.warn("aborting notification exec_create() because docker exec failed")
155             return "notification unsuccessful"  # failed to get an exec_id, perhaps trying multiple times, so don't raise or CM will retry usually forever
156         DockerClient._logger.debug("notification exec_create() succeeded")
157
158         for attempts_remaining in range(11,-1,-1):
159             try:
160                 result = self._client.exec_start(exec_id=result['Id'])
161             except Exception as e:
162                 DockerClient._logger.debug("notification exec_start() got exception {}: {!s}".format(type(e).__name__, e))
163                 if attempts_remaining == 0:
164                     DockerClient._logger.warn("aborting notification exec_start() because exception {}: {!s}".format(type(e).__name__, e))
165                     return str(e)  # don't raise or CM will retry usually forever
166                     # raise DockerClientError(e)
167                 time.sleep(10)
168             else:
169                 break
170         DockerClient._logger.debug("notification exec_start() succeeded")
171
172         DockerClient._logger.info("Pass to docker exec {} {} {}".format(
173             container_id, cmd, result))
174
175         return result