Do first pass on dcae onboarding vagrant
[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
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, logins=[]):
78     hostname, port = profile.docker_host.split(":")
79     try:
80         client = doc.create_client(hostname, port, logins=logins)
81         client.ping()
82         return client
83     except:
84         raise DockerError('Could not connect to the Docker daemon. Is it running?')
85
86
87 def image_exists(image):
88     '''Returns True if the image exists locally'''
89     client = docker.from_env(version="auto")
90     return True if client.images(image) else False
91
92
93 def _infer_ip():
94     '''Infers the IP address of the host running this tool'''
95     if not platform.startswith('linux'):
96         raise DockerError('Non-linux environment detected. Use the --external-ip flag when running Docker components.')
97     ip = socket.gethostbyname(socket.gethostname())
98     dlog.info("Docker host external IP address inferred to be {:}. If this is incorrect, use the --external-ip flag.".format(ip))
99     return ip
100
101
102 def _run_container(client, config, name=None, wait=False):
103     '''Runs a container'''
104     if name is not None:
105         info = six.next(iter(client.containers(all=True, filters={'name': "^/{:}$".format(name)})), None)
106         if info is not None:
107             if info['State'] == 'running':
108                 dlog.info("Container '{:}' was detected as already running.".format(name))
109                 return info
110             else:
111                 client.remove_container(info['Id'])
112
113     cont = doc.create_container_using_config(client, name, config)
114     client.start(cont)
115     info = client.inspect_container(cont)
116     name = info['Name'][1:]  # remove '/' prefix
117     image = config['Image']
118     dlog.info("Running image '{:}' as '{:}'".format(image, name))
119
120     if not wait:
121         return info
122
123     cont_log = dlog.getChild(name)
124     try:
125         for msg in client.logs(cont, stream=True):
126             cont_log.info(msg.decode())
127         else:
128             dlog.info("Container '{:}' exitted suddenly.".format(name))
129     except (KeyboardInterrupt, SystemExit):
130         dlog.info("Stopping container '{:}' and cleaning up...".format(name))
131         client.kill(cont)
132         client.remove_container(cont)
133
134
135 def _run_registrator(client, external_ip=None):
136     '''Ensures that Registrator is running'''
137
138     ip = _infer_ip() if external_ip is None else external_ip
139     cmd = _reg_cmd.format(ip).split()
140
141     binds={'/var/run/docker.sock': {'bind': '/tmp/docker.sock'}}
142     hconf = client.create_host_config(binds=binds, network_mode='host')
143     conf = client.create_container_config(image=_reg_img, command=cmd, host_config=hconf)
144
145     _run_container(client, conf, name='registrator', wait=False)
146
147
148 # TODO: Need to revisit and reimplement _run_registrator(client, external_ip)
149
150 #
151 # High level calls
152 #
153
154 def deploy_component(profile, image, instance_name, docker_config, should_wait=False,
155         logins=[]):
156     """Deploy Docker component
157
158     This calls runs a Docker container detached.  The assumption is that the Docker
159     host already has registrator running.
160
161     TODO: Split out the wait functionality
162
163     Args
164     ----
165     logins (list): List of objects where the objects are each a docker login of
166     the form:
167
168         {"registry": .., "username":.., "password":.. }
169
170     Returns
171     -------
172     Dict that is the result from a Docker inspect call
173     """
174     ports = docker_config.get("ports", None)
175     hcp = doc.add_host_config_params_ports(ports=ports)
176     volumes = docker_config.get("volumes", None)
177     hcp = doc.add_host_config_params_volumes(volumes=volumes, host_config_params=hcp)
178     # Thankfully passing in an IP will return back an IP
179     dh = profile.docker_host.split(":")[0]
180     _, _, dhips = socket.gethostbyname_ex(dh)
181
182     if dhips:
183         hcp = doc.add_host_config_params_dns(dhips[0], hcp)
184     else:
185         raise DockerConstructionError("Could not resolve the docker hostname:{0}".format(dh))
186
187     envs = build_envs(profile, docker_config, instance_name)
188     client = get_docker_client(profile, logins=logins)
189
190     config = doc.create_container_config(client, image, envs, hcp)
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