diff --git a/NEWS b/NEWS index 7ecc1530..81ee3911 100644 --- a/NEWS +++ b/NEWS @@ -10,7 +10,8 @@ Version 2.3 * Add Statsd export module (--export-statsd) (issue #465) * Refactor export module (CSV export option is now --export-csv). It is now possible to export stats from the Glances client mode (issue #463) * The Web inteface is now based on BootStarp / RWD grid (issue #417, #366 and #461) Thanks to Nicolas Hart @nclsHart - * Add the RAID plugins (issue #447) + * Add the RAID plugin (issue #447) + * Add the Docker plugin (issue #440) Version 2.2.1 ============= diff --git a/README.rst b/README.rst index faff1001..c1f309cd 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,7 @@ Optional dependencies: - ``influxdb`` (for the InfluxDB export module) - ``statsd`` (for the StatsD export module) - ``pystache`` (for the action script feature) +- ``docker-py`` (for the Docker monitoring support) [Linux-only] Installation ============ diff --git a/docs/glances-doc.rst b/docs/glances-doc.rst index 7ea6c134..c86b1c7b 100644 --- a/docs/glances-doc.rst +++ b/docs/glances-doc.rst @@ -671,6 +671,13 @@ Each alert message displays the following information: 4. {min,avg,max} values or number of running processes for monitored processes list alerts +Docker +------ + +If you use Docker, Glances can help you to monitor your container. Glances uses the Docker API through the Docker-Py library. + +.. image:: images/docker.png + Actions ------- diff --git a/docs/images/docker.png b/docs/images/docker.png new file mode 100644 index 00000000..e8115359 Binary files /dev/null and b/docs/images/docker.png differ diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index 7a5b016f..2a275c0c 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -77,6 +77,8 @@ class GlancesMain(object): dest='disable_sensors', help=_('disable sensors module')) parser.add_argument('--disable-raid', action='store_true', default=False, dest='disable_raid', help=_('disable RAID module')) + parser.add_argument('--disable-docker', action='store_true', default=False, + dest='disable_docker', help=_('disable Docker module')) parser.add_argument('--disable-left-sidebar', action='store_true', default=False, dest='disable_left_sidebar', help=_('disable network, disk io, FS and sensors modules')) parser.add_argument('--disable-process', action='store_true', default=False, diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 378dc64b..c98b7a5b 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -266,6 +266,9 @@ class _GlancesCurses(object): elif self.pressedkey == ord('d'): # 'd' > Show/hide disk I/O stats self.args.disable_diskio = not self.args.disable_diskio + elif self.pressedkey == ord('D'): + # 'D' > Show/hide Docker stats + self.args.disable_docker = not self.args.disable_docker elif self.pressedkey == ord('e'): # 'e' > Enable/Disable extended stats for top process self.args.enable_process_extended = not self.args.enable_process_extended @@ -432,6 +435,8 @@ class _GlancesCurses(object): stats_sensors = stats.get_plugin( 'sensors').get_stats_display(args=self.args) stats_now = stats.get_plugin('now').get_stats_display() + stats_docker = stats.get_plugin('docker').get_stats_display( + args=self.args) stats_processcount = stats.get_plugin( 'processcount').get_stats_display(args=self.args) stats_monitor = stats.get_plugin( @@ -441,7 +446,8 @@ class _GlancesCurses(object): # Adapt number of processes to the available space max_processes_displayed = screen_y - 11 - \ - self.get_stats_display_height(stats_alert) + self.get_stats_display_height(stats_alert) - \ + self.get_stats_display_height(stats_docker) if self.args.enable_process_extended and not self.args.process_tree: max_processes_displayed -= 4 if max_processes_displayed < 0: @@ -534,9 +540,11 @@ class _GlancesCurses(object): self.next_line = self.saved_line # Display right sidebar - # (PROCESS_COUNT+MONITORED+PROCESS_LIST+ALERT) + # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT) self.new_column() self.new_line() + self.display_plugin(stats_docker) + self.new_line() self.display_plugin(stats_processcount) if glances_processes.get_process_filter() is None and cs_status == 'None': # Do not display stats monitor list if a filter exist diff --git a/glances/plugins/glances_batpercent.py b/glances/plugins/glances_batpercent.py index ff1deecc..e2ced95c 100644 --- a/glances/plugins/glances_batpercent.py +++ b/glances/plugins/glances_batpercent.py @@ -56,6 +56,7 @@ class Plugin(GlancesPlugin): """Reset/init the stats.""" self.stats = [] + @GlancesPlugin._log_result_decorator def update(self): """Update battery capacity stats using the input method.""" # Reset stats diff --git a/glances/plugins/glances_docker.py b/glances/plugins/glances_docker.py new file mode 100644 index 00000000..edd01081 --- /dev/null +++ b/glances/plugins/glances_docker.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2015 Nicolargo +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Docker plugin.""" + +# Import Glances libs +from glances.core.glances_logging import logger +from glances.plugins.glances_plugin import GlancesPlugin + +# 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 +else: + docker_tag = True + + +class Plugin(GlancesPlugin): + + """Glances' Docker plugin. + + stats is a list + """ + + def __init__(self, args=None): + """Init the plugin.""" + GlancesPlugin.__init__(self, args=args) + + # The plgin can be disable using: args.disable_docker + self.args = args + + # We want to display the stat in the curse interface + self.display_curse = True + + # Init the Docker API + self.docker_client = self.connect() + if self.docker_client is None: + global docker_tag + docker_tag = False + + def connect(self, version=None): + """Connect to the Docker server""" + # Init connection to the Docker API + if version is None: + ret = docker.Client(base_url='unix://var/run/docker.sock') + else: + ret = docker.Client(base_url='unix://var/run/docker.sock', + version=version) + try: + ret.version() + except requests.exceptions.ConnectionError as e: + # Connexion error (Docker not detected) + # Let this message in debug mode + logger.debug("Can't connect to the Docker server (%s)" % e) + ret = None + except docker.errors.APIError as e: + if version is None: + # API error (Version mismatch ?) + logger.debug("Docker API error (%s)" % e) + # Try the connection with the server version + import re + version = re.search('server\:\ (.*)\)\"\)', str(e)) + if version: + logger.debug("Try connection with Docker API version %s" % version.group(1)) + ret = self.connect(version=version.group(1)) + else: + # API error + logger.error("Docker API error (%s)" % e) + ret = None + except Exception as e: + # Others exceptions... + # Connexion error (Docker not detected) + logger.error("Can't connect to the Docker server (%s)" % e) + ret = None + + return ret + + def reset(self): + """Reset/init the stats.""" + self.stats = {} + + @GlancesPlugin._log_result_decorator + def update(self): + """Update Docker stats using the input method. + """ + # Reset stats + self.reset() + + # The Docker-py lib is mandatory + if not docker_tag or self.args.disable_docker: + return self.stats + + if self.get_input() == 'local': + # Update stats + # Exemple: { + # "KernelVersion": "3.16.4-tinycore64", + # "Arch": "amd64", + # "ApiVersion": "1.15", + # "Version": "1.3.0", + # "GitCommit": "c78088f", + # "Os": "linux", + # "GoVersion": "go1.3.3" + # } + self.stats['version'] = self.docker_client.version() + # 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'}] + self.stats['containers'] = self.docker_client.containers() + + elif self.get_input() == 'snmp': + # Update stats using SNMP + # Not available + pass + + return self.stats + + def msg_curse(self, args=None): + """Return the dict to display in the curse interface.""" + # Init the return message + ret = [] + + # Only process if stats exist (and non null) and display plugin enable... + if self.stats == {} or args.disable_docker or len(self.stats['containers']) == 0: + return ret + + # Build the string message + # Title + msg = '{0}'.format(_("CONTAINERS")) + ret.append(self.curse_add_line(msg, "TITLE")) + msg = ' {0}'.format(len(self.stats['containers'])) + ret.append(self.curse_add_line(msg)) + msg = ' ({0} {1})'.format(_("served by Docker"), + self.stats['version']["Version"]) + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + # Header + ret.append(self.curse_new_line()) + msg = '{0:>14}'.format(_("Id")) + ret.append(self.curse_add_line(msg)) + msg = ' {0:20}'.format(_("Name")) + ret.append(self.curse_add_line(msg)) + msg = '{0:>26}'.format(_("Status")) + ret.append(self.curse_add_line(msg)) + msg = ' {0:8}'.format(_("Command")) + ret.append(self.curse_add_line(msg)) + # Data + for container in self.stats['containers']: + ret.append(self.curse_new_line()) + # Id + msg = '{0:>14}'.format(container['Id'][0:12]) + ret.append(self.curse_add_line(msg)) + # Name + name = container['Names'][0] + if len(name) > 20: + name = '_' + name[:-19] + else: + name[0:20] + msg = ' {0:20}'.format(name) + ret.append(self.curse_add_line(msg)) + # Status + status = self.container_alert(container['Status']) + msg = container['Status'].replace("minute", "min") + msg = '{0:>26}'.format(msg[0:25]) + ret.append(self.curse_add_line(msg, status)) + # Command + msg = ' {0}'.format(container['Command']) + ret.append(self.curse_add_line(msg)) + + return ret + + def container_alert(self, status): + """Analyse the container status""" + if "Paused" in status: + return 'CAREFUL' + else: + return 'OK' diff --git a/glances/plugins/glances_help.py b/glances/plugins/glances_help.py index 02504b1d..bf1f18f6 100644 --- a/glances/plugins/glances_help.py +++ b/glances/plugins/glances_help.py @@ -139,6 +139,10 @@ class Plugin(GlancesPlugin): ret.append(self.curse_new_line()) msg = msg_col.format("/", _("Enable/disable short processes name")) ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + msg = msg_col.format("D", _("Enable/disable Docker stats")) + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) ret.append(self.curse_new_line())