diff --git a/NEWS b/NEWS index 820080c8..b226aeb5 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,10 @@ Bugs corrected: * Failed to connect to bus: No such file or directory #1156 +Backward-incompatible changes: + + * Support for Docker API < 2.0 is deprecated (docker plugins) + ============================================================================== Glances Version 2 ============================================================================== diff --git a/glances/plugins/glances_docker.py b/glances/plugins/glances_docker.py index 00fe13f6..83640713 100644 --- a/glances/plugins/glances_docker.py +++ b/glances/plugins/glances_docker.py @@ -20,7 +20,6 @@ """Docker plugin.""" import os -import re import threading import time @@ -29,13 +28,10 @@ from glances.logger import logger from glances.timer import getTimeSinceLastUpdate from glances.plugins.glances_plugin import GlancesPlugin -from glances.globals import WINDOWS - # Docker-py library (optional and Linux-only) # https://github.com/docker/docker-py try: import docker - import requests except ImportError as e: logger.debug("Docker library not found (%s). Glances cannot grab Docker info." % e) docker_tag = False @@ -47,7 +43,7 @@ class Plugin(GlancesPlugin): """Glances Docker plugin. - stats is a list + stats is a dict: {'version': {...}, 'containers': [{}, {}]} """ def __init__(self, args=None): @@ -61,7 +57,7 @@ class Plugin(GlancesPlugin): self.display_curse = True # Init the Docker API - self.docker_client = False + self.docker_client = self.connect() # Dict of thread (to grab stats asynchroniously, one thread is created by container) # key: Container Id @@ -95,79 +91,13 @@ class Plugin(GlancesPlugin): logger.debug("docker plugin - Docker export error {}".format(e)) return ret - def __connect_old(self, version): - """Connect to the Docker server with the 'old school' method""" - # Glances is compatible with both API 2.0 and <2.0 - # (thanks to the @bacondropped patch) - if hasattr(docker, 'APIClient'): - # Correct issue #1000 for API 2.0 - init_docker = docker.APIClient - elif hasattr(docker, 'Client'): - # < API 2.0 - init_docker = docker.Client - else: - # Can not found init method (new API version ?) - logger.error("docker plugin - Can not found any way to init the Docker API") - return None - # Init connection to the Docker API - try: - if WINDOWS: - url = 'npipe:////./pipe/docker_engine' - else: - url = 'unix://var/run/docker.sock' - if version is None: - ret = init_docker(base_url=url) - else: - ret = init_docker(base_url=url, - version=version) - except NameError: - # docker lib not found - return None - - return ret - - def connect(self, version=None): + def connect(self): """Connect to the Docker server.""" - if hasattr(docker, 'from_env') and version is not None: - # Connect to Docker using the default socket or - # the configuration in your environment - ret = docker.from_env() - else: - ret = self.__connect_old(version=version) + global docker_tag - # Check the server connection with the version() method - try: - ret.version() - except requests.exceptions.ConnectionError as e: - # Connexion error (Docker not detected) - # Let this message in debug mode - logger.debug("docker plugin - Can't connect to the Docker server (%s)" % e) - return None - except docker.errors.APIError as e: - if version is None: - # API error (Version mismatch ?) - logger.debug("docker plugin - Docker API error (%s)" % e) - # Try the connection with the server version - version = re.search('(?:server API version|server)\:\ (.*)\)\".*\)', str(e)) - if version: - logger.debug("docker plugin - Try connection with Docker API version %s" % version.group(1)) - ret = self.connect(version=version.group(1)) - else: - logger.debug("docker plugin - Can not retreive Docker server version") - ret = None - else: - # API error - logger.error("docker plugin - Docker API error (%s)" % e) - ret = None - except Exception as e: - # Others exceptions... - # Connexion error (Docker not detected) - logger.error("docker plugin - Can't connect to the Docker server (%s)" % e) - ret = None + ret = docker.from_env() - # Log an info if Docker plugin is disabled - if ret is None: - logger.debug("docker plugin - Docker plugin is disable because an error has been detected") + # TODO: manage connection error return ret @@ -179,22 +109,10 @@ class Plugin(GlancesPlugin): @GlancesPlugin._log_result_decorator def update(self): """Update Docker stats using the input method.""" - global docker_tag # Reset stats self.reset() - # Get the current Docker API client - if not self.docker_client: - # First time, try to connect to the server - try: - self.docker_client = self.connect() - except Exception: - docker_tag = False - else: - if self.docker_client is None: - docker_tag = False - # The Docker-py lib is mandatory if not docker_tag: return self.stats @@ -219,35 +137,27 @@ class Plugin(GlancesPlugin): logger.error("{} plugin - Cannot get Docker version ({})".format(self.plugin_name, e)) return self.stats - # Container globals information - # Example: [{u'Status': u'Up 36 seconds', - # u'Created': 1420378904, - # u'Image': u'nginx:1', - # u'Ports': [{u'Type': u'tcp', u'PrivatePort': 443}, - # {u'IP': u'0.0.0.0', u'Type': u'tcp', u'PublicPort': 8080, u'PrivatePort': 80}], - # u'Command': u"nginx -g 'daemon off;'", - # u'Names': [u'/webstack_nginx_1'], - # u'Id': u'b0da859e84eb4019cf1d965b15e9323006e510352c402d2f442ea632d61faaa5'}] - # Update current containers list try: - self.stats['containers'] = self.docker_client.containers() or [] + # Issue #1152: Docker module doesn't export details about stopped containers + # It could be done here by setting all=True but the list is too long... + containers = self.docker_client.containers.list(all=False) or [] except Exception as e: logger.error("{} plugin - Cannot get containers list ({})".format(self.plugin_name, e)) return self.stats # Start new thread for new container - for container in self.stats['containers']: - if container['Id'] not in self.thread_list: + for container in containers: + if container.id not in self.thread_list: # Thread did not exist in the internal dict # Create it and add it to the internal dict - logger.debug("{} plugin - Create thread for container {}".format(self.plugin_name, container['Id'][:12])) - t = ThreadDockerGrabber(self.docker_client, container['Id']) - self.thread_list[container['Id']] = t + logger.debug("{} plugin - Create thread for container {}".format(self.plugin_name, container.id[:12])) + t = ThreadDockerGrabber(container) + self.thread_list[container.id] = t t.start() # Stop threads for non-existing containers - nonexisting_containers = set(iterkeys(self.thread_list)) - set([c['Id'] for c in self.stats['containers']]) + nonexisting_containers = set(iterkeys(self.thread_list)) - set([c.id for c in containers]) for container_id in nonexisting_containers: # Stop the thread logger.debug("{} plugin - Stop thread for old container {}".format(self.plugin_name, container_id[:12])) @@ -256,17 +166,30 @@ class Plugin(GlancesPlugin): del self.thread_list[container_id] # Get stats for all containers - for container in self.stats['containers']: + self.stats['containers'] = [] + for container in containers: + # Init the stats for the current container + container_stats = {} # The key is the container name and not the Id - container['key'] = self.get_key() - + container_stats['key'] = self.get_key() # Export name (first name in the list, without the /) - container['name'] = container['Names'][0][1:] - - container['cpu'] = self.get_docker_cpu(container['Id'], self.thread_list[container['Id']].stats) - container['memory'] = self.get_docker_memory(container['Id'], self.thread_list[container['Id']].stats) - container['network'] = self.get_docker_network(container['Id'], self.thread_list[container['Id']].stats) - container['io'] = self.get_docker_io(container['Id'], self.thread_list[container['Id']].stats) + container_stats['name'] = container.name + # Global stats (from attrs) + container_stats['Status'] = container.attrs['State']['Status'] + container_stats['Command'] = container.attrs['Config']['Entrypoint'] + # Standards stats + if container_stats['Status'] in ('running', 'paused'): + container_stats['cpu'] = self.get_docker_cpu(container.id, self.thread_list[container.id].stats) + container_stats['memory'] = self.get_docker_memory(container.id, self.thread_list[container.id].stats) + container_stats['network'] = self.get_docker_network(container.id, self.thread_list[container.id].stats) + container_stats['io'] = self.get_docker_io(container.id, self.thread_list[container.id].stats) + else: + container_stats['cpu'] = {} + container_stats['memory'] = {} + container_stats['network'] = {} + container_stats['io'] = {} + # Add current container stats to the stats list + self.stats['containers'].append(container_stats) elif self.input_method == 'snmp': # Update stats using SNMP @@ -454,7 +377,7 @@ class Plugin(GlancesPlugin): iow = [i for i in iocounters['io_service_bytes_recursive'] if i['op'] == 'Write'][0]['value'] ior_old = [i for i in self.iocounters_old[container_id]['io_service_bytes_recursive'] if i['op'] == 'Read'][0]['value'] iow_old = [i for i in self.iocounters_old[container_id]['io_service_bytes_recursive'] if i['op'] == 'Write'][0]['value'] - except (IndexError, KeyError) as e: + except (IndexError, KeyError, TypeError) as e: # all_stats do not have io information logger.debug("docker plugin - Cannot grab block IO usage for container {} ({})".format(container_id, e)) else: @@ -634,10 +557,14 @@ class Plugin(GlancesPlugin): def container_alert(self, status): """Analyse the container status.""" - if "Paused" in status: - return 'CAREFUL' - else: + if status in ('running'): return 'OK' + elif status in ('exited'): + return 'WARNING' + elif status in ('dead'): + return 'CRITICAL' + else: + return 'CAREFUL' class ThreadDockerGrabber(threading.Thread): @@ -647,19 +574,19 @@ class ThreadDockerGrabber(threading.Thread): stats is a dict """ - def __init__(self, docker_client, container_id): + def __init__(self, container): """Init the class: - docker_client: instance of Docker-py client - container_id: Id of the container""" - logger.debug("docker plugin - Create thread for container {}".format(container_id[:12])) + container: instance of Docker-py Container + """ super(ThreadDockerGrabber, self).__init__() # Event needed to stop properly the thread self._stopper = threading.Event() # The docker-py return stats as a stream - self._container_id = container_id - self._stats_stream = docker_client.stats(container_id, decode=True) + self._container = container + self._stats_stream = container.stats(decode=True) # The class return the stats as a dict self._stats = {} + logger.debug("docker plugin - Create thread for container {}".format(self._container.name)) def run(self): """Function called to grab stats. @@ -683,7 +610,7 @@ class ThreadDockerGrabber(threading.Thread): def stop(self, timeout=None): """Stop the thread""" - logger.debug("docker plugin - Close thread for container {}".format(self._container_id[:12])) + logger.debug("docker plugin - Close thread for container {}".format(self._container.name)) self._stopper.set() def stopped(self):