Policy Reconfiguration, Component Spec, Help text
[dcaegen2/platform/cli.git] / dcae-cli / dcae_cli / util / docker_util.py
1 # ============LICENSE_START=======================================================
2 # org.onap.dcae
3 # ================================================================================
4 # Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved.
5 # ================================================================================
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #      http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 # ============LICENSE_END=========================================================
18 #
19 # ECOMP is a trademark and service mark of AT&T Intellectual Property.
20
21 # -*- coding: utf-8 -*-
22 """
23 Provides utilities for Docker components
24 """
25 import socket
26 from sys import platform
27
28 import docker
29 import six
30
31 import dockering as doc
32 from dcae_cli.util.logger import get_logger
33 from dcae_cli.util.exc import DcaeException
34
35 dlog = get_logger('Docker')
36
37 _reg_img = 'gliderlabs/registrator:latest'
38 # TODO: Source this from app's configuration [ONAP URL TBD]
39 _reg_cmd = '-ip {:} consul://make-me-valid:8500'
40
41 class DockerError(DcaeException):
42     pass
43
44 class DockerConstructionError(DcaeException):
45     pass
46
47
48 # Functions to provide envs to pass into Docker containers
49
50 def _convert_profile_to_docker_envs(profile):
51     """Convert a profile object to Docker environment variables
52
53     Parameters
54     ----------
55     profile: namedtuple
56
57     Returns
58     -------
59     dict of environemnt variables to be used by docker-py
60     """
61     profile = profile._asdict()
62     return dict([(key.upper(), value) for key, value in six.iteritems(profile)])
63
64
65 def build_envs(profile, docker_config, instance_name):
66     profile_envs = _convert_profile_to_docker_envs(profile)
67     health_envs = doc.create_envs_healthcheck(docker_config)
68     return doc.create_envs(instance_name, profile_envs, health_envs)
69
70
71 # Methods to call Docker engine
72
73 # TODO: Consolidate these two docker client methods. Need ability to invoke local
74 # vs remote Docker engine
75
76 def get_docker_client(profile, logins=[]):
77     hostname, port = profile.docker_host.split(":")
78     try:
79         client = doc.create_client(hostname, port, logins=logins)
80         client.ping()
81         return client
82     except:
83         raise DockerError('Could not connect to the Docker daemon. Is it running?')
84
85
86 def image_exists(image):
87     '''Returns True if the image exists locally'''
88     client = docker.from_env(version="auto")
89     return True if client.images(image) else False
90
91
92 def _infer_ip():
93     '''Infers the IP address of the host running this tool'''
94     if not platform.startswith('linux'):
95         raise DockerError('Non-linux environment detected. Use the --external-ip flag when running Docker components.')
96     ip = socket.gethostbyname(socket.gethostname())
97     dlog.info("Docker host external IP address inferred to be {:}. If this is incorrect, use the --external-ip flag.".format(ip))
98     return ip
99
100
101 def _run_container(client, config, name=None, wait=False):
102     '''Runs a container'''
103     if name is not None:
104         info = six.next(iter(client.containers(all=True, filters={'name': "^/{:}$".format(name)})), None)
105         if info is not None:
106             if info['State'] == 'running':
107                 dlog.info("Container '{:}' was detected as already running.".format(name))
108                 return info
109             else:
110                 client.remove_container(info['Id'])
111
112     cont = doc.create_container_using_config(client, name, config)
113     client.start(cont)
114     info = client.inspect_container(cont)
115     name = info['Name'][1:]  # remove '/' prefix
116     image = config['Image']
117     dlog.info("Running image '{:}' as '{:}'".format(image, name))
118
119     if not wait:
120         return info
121
122     cont_log = dlog.getChild(name)
123     try:
124         for msg in client.logs(cont, stream=True):
125             cont_log.info(msg.decode())
126         else:
127             dlog.info("Container '{:}' exitted suddenly.".format(name))
128     except (KeyboardInterrupt, SystemExit):
129         dlog.info("Stopping container '{:}' and cleaning up...".format(name))
130         client.kill(cont)
131         client.remove_container(cont)
132
133
134 def _run_registrator(client, external_ip=None):
135     '''Ensures that Registrator is running'''
136
137     ip = _infer_ip() if external_ip is None else external_ip
138     cmd = _reg_cmd.format(ip).split()
139
140     binds={'/var/run/docker.sock': {'bind': '/tmp/docker.sock'}}
141     hconf = client.create_host_config(binds=binds, network_mode='host')
142     conf = client.create_container_config(image=_reg_img, command=cmd, host_config=hconf)
143
144     _run_container(client, conf, name='registrator', wait=False)
145
146
147 # TODO: Need to revisit and reimplement _run_registrator(client, external_ip)
148
149 #
150 # High level calls
151 #
152
153 def deploy_component(profile, image, instance_name, docker_config, should_wait=False,
154         logins=[]):
155     """Deploy Docker component
156
157     This calls runs a Docker container detached.  The assumption is that the Docker
158     host already has registrator running.
159
160     TODO: Split out the wait functionality
161
162     Args
163     ----
164     logins (list): List of objects where the objects are each a docker login of
165     the form:
166
167         {"registry": .., "username":.., "password":.. }
168
169     Returns
170     -------
171     Dict that is the result from a Docker inspect call
172     """
173     ports = docker_config.get("ports", None)
174     hcp = doc.add_host_config_params_ports(ports=ports)
175     volumes = docker_config.get("volumes", None)
176     hcp = doc.add_host_config_params_volumes(volumes=volumes, host_config_params=hcp)
177     # Thankfully passing in an IP will return back an IP
178     dh = profile.docker_host.split(":")[0]
179     _, _, dhips = socket.gethostbyname_ex(dh)
180
181     if dhips:
182         hcp = doc.add_host_config_params_dns(dhips[0], hcp)
183     else:
184         raise DockerConstructionError("Could not resolve the docker hostname:{0}".format(dh))
185
186     envs = build_envs(profile, docker_config, instance_name)
187     client = get_docker_client(profile, logins=logins)
188
189     config = doc.create_container_config(client, image, envs, hcp)
190
191     return _run_container(client, config, name=instance_name, wait=should_wait)
192
193
194 def undeploy_component(client, image, instance_name):
195     """Undeploy Docker component
196
197     TODO: Handle error scenarios. Look into:
198     * no container found error - docker.errors.NotFound
199     * failure to remove image - docker.errors.APIError: 409 Client Error
200     * retry, check for still running container
201
202     Returns
203     -------
204     True if the container and associated image has been removed False otherwise
205     """
206     try:
207         client.remove_container(instance_name, force=True)
208         client.remove_image(image)
209         return True
210     except Exception as e:
211         dlog.error("Error while undeploying Docker container/image: {0}".format(e))
212         return False
213
214 def reconfigure(client, instance_name, command):
215     """  Execute the Reconfig script in the Docker container  """
216
217     #  'command' has 3 parts in a list (1 Command and 2 ARGs)
218     exec_Id = client.exec_create(container=instance_name, cmd=command)
219
220     exec_start_resp = client.exec_start(exec_Id, stream=True)
221
222     #  Using a 'single' generator response to solve issue of 'start_exec' returning control after 6 minutes
223     for response in exec_start_resp:
224         dlog.info("Reconfig Script execution response: {:}".format(response))
225         exec_start_resp.close()
226         break