Add dcae-cli and component-json-schemas projects
[dcaegen2/platform/cli.git] / dcae-cli / dcae_cli / util / docker_util.py
1 # ============LICENSE_START=======================================================
2 # org.onap.dcae
3 # ================================================================================
4 # Copyright (c) 2017 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
36 dlog = get_logger('Docker')
37
38 _reg_img = 'gliderlabs/registrator:latest'
39 # TODO: Source this from app's configuration [ONAP URL TBD]
40 _reg_cmd = '-ip {:} consul://make-me-valid:8500'
41
42 class DockerError(DcaeException):
43     pass
44
45 class DockerConstructionError(DcaeException):
46     pass
47
48
49 # Functions to provide envs to pass into Docker containers
50
51 def _convert_profile_to_docker_envs(profile):
52     """Convert a profile object to Docker environment variables
53
54     Parameters
55     ----------
56     profile: namedtuple
57
58     Returns
59     -------
60     dict of environemnt variables to be used by docker-py
61     """
62     profile = profile._asdict()
63     return dict([(key.upper(), value) for key, value in six.iteritems(profile)])
64
65
66 def build_envs(profile, docker_config, instance_name):
67     profile_envs = _convert_profile_to_docker_envs(profile)
68     health_envs = doc.create_envs_healthcheck(docker_config)
69     return doc.create_envs(instance_name, profile_envs, health_envs)
70
71
72 # Methods to call Docker engine
73
74 # TODO: Consolidate these two docker client methods. Need ability to invoke local
75 # vs remote Docker engine
76
77 def get_docker_client(profile):
78     base_url = "tcp://{0}".format(profile.docker_host)
79     try:
80         client = docker.Client(base_url=base_url)
81         client.ping()
82         return client
83     except:
84         raise DockerError('Could not connect to the Docker daemon. Is it running?')
85
86 def _get_docker_client(client_funcs=(docker.Client, docker.from_env)):
87     '''Returns a docker client object'''
88     for func in client_funcs:
89         try:
90             client = func(version='auto')
91             client.ping()
92             return client
93         except:
94             continue
95     raise DockerError('Could not connect to the Docker daemon. Is it running?')
96
97
98 def image_exists(image):
99     '''Returns True if the image exists locally'''
100     client = _get_docker_client()
101     return True if client.images(image) else False
102
103
104 def _infer_ip():
105     '''Infers the IP address of the host running this tool'''
106     if not platform.startswith('linux'):
107         raise DockerError('Non-linux environment detected. Use the --external-ip flag when running Docker components.')
108     ip = socket.gethostbyname(socket.gethostname())
109     dlog.info("Docker host external IP address inferred to be {:}. If this is incorrect, use the --external-ip flag.".format(ip))
110     return ip
111
112
113 def _run_container(client, config, name=None, wait=False):
114     '''Runs a container'''
115     if name is not None:
116         info = six.next(iter(client.containers(all=True, filters={'name': "^/{:}$".format(name)})), None)
117         if info is not None:
118             if info['State'] == 'running':
119                 dlog.info("Container '{:}' was detected as already running.".format(name))
120                 return info
121             else:
122                 client.remove_container(info['Id'])
123
124     cont = doc.create_container_using_config(client, name, config)
125     client.start(cont)
126     info = client.inspect_container(cont)
127     name = info['Name'][1:]  # remove '/' prefix
128     image = config['Image']
129     dlog.info("Running image '{:}' as '{:}'".format(image, name))
130
131     if not wait:
132         return info
133
134     cont_log = dlog.getChild(name)
135     try:
136         for msg in client.logs(cont, stream=True):
137             cont_log.info(msg.decode())
138         else:
139             dlog.info("Container '{:}' exitted suddenly.".format(name))
140     except (KeyboardInterrupt, SystemExit):
141         dlog.info("Stopping container '{:}' and cleaning up...".format(name))
142         client.kill(cont)
143         client.remove_container(cont)
144
145
146 def _run_registrator(client, external_ip=None):
147     '''Ensures that Registrator is running'''
148
149     ip = _infer_ip() if external_ip is None else external_ip
150     cmd = _reg_cmd.format(ip).split()
151
152     binds={'/var/run/docker.sock': {'bind': '/tmp/docker.sock'}}
153     hconf = client.create_host_config(binds=binds, network_mode='host')
154     conf = client.create_container_config(image=_reg_img, command=cmd, host_config=hconf)
155
156     _run_container(client, conf, name='registrator', wait=False)
157
158
159 # TODO: Need to revisit and reimplement _run_registrator(client, external_ip)
160
161 #
162 # High level calls
163 #
164
165 def deploy_component(profile, image, instance_name, docker_config, should_wait=False):
166     """Deploy Docker component
167
168     This calls runs a Docker container detached.  The assumption is that the Docker
169     host already has registrator running.
170
171     TODO: Split out the wait functionality
172
173     Returns
174     -------
175     Dict that is the result from a Docker inspect call
176     """
177     ports = docker_config.get("ports", None)
178     hcp = doc.add_host_config_params_ports(ports=ports)
179     volumes = docker_config.get("volumes", None)
180     hcp = doc.add_host_config_params_volumes(volumes=volumes, host_config_params=hcp)
181     # Thankfully passing in an IP will return back an IP
182     dh = profile.docker_host.split(":")[0]
183     _, _, dhips = socket.gethostbyname_ex(dh)
184
185     if dhips:
186         hcp = doc.add_host_config_params_dns(dhips[0], hcp)
187     else:
188         raise DockerConstructionError("Could not resolve the docker hostname:{0}".format(dh))
189
190     envs = build_envs(profile, docker_config, instance_name)
191     client = get_docker_client(profile)
192
193     config = doc.create_container_config(client, image, envs, hcp)
194     return _run_container(client, config, name=instance_name, wait=should_wait)
195
196
197 def undeploy_component(client, image, instance_name):
198     """Undeploy Docker component
199
200     TODO: Handle error scenarios. Look into:
201     * no container found error - docker.errors.NotFound
202     * failure to remove image - docker.errors.APIError: 409 Client Error
203     * retry, check for still running container
204
205     Returns
206     -------
207     True if the container and associated image has been removed False otherwise
208     """
209     try:
210         client.remove_container(instance_name, force=True)
211         client.remove_image(image)
212         return True
213     except Exception as e:
214         dlog.error("Error while undeploying Docker container/image: {0}".format(e))
215         return False