From 7acca165fddc699194497f99af2608bd837a0943 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sun, 12 Feb 2017 18:49:25 +0100 Subject: [PATCH] First part of the implementation of the issue #1029 (all but the Web UI integration --- NEWS | 1 + docs/aoa/header.rst | 3 + docs/man/glances.1 | 2 +- glances/main.py | 2 + glances/outputs/glances_curses.py | 17 ++- glances/plugins/glances_cloud.py | 173 ++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 glances/plugins/glances_cloud.py diff --git a/NEWS b/NEWS index 30944f89..a008ac48 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,7 @@ Enhancements and new features: * Use new sensors-related APIs of Psutil 5.1.0 (issue #1018) => Remove Py3Sensors and Batinfo dependencies + * Add a "Cloud" plugin to grab stats inside the AWS EC2 API (issue #1029) Bugs corrected: diff --git a/docs/aoa/header.rst b/docs/aoa/header.rst index 3342e4bf..46a21b5f 100644 --- a/docs/aoa/header.rst +++ b/docs/aoa/header.rst @@ -11,6 +11,9 @@ Additionally, on GNU/Linux, it also shows the kernel version. In client mode, the server connection status is also displayed. +If you are hosted on an AWS EC2 instance, some additional information +can be displayed (AMI-ID, region). + **Connected**: .. image:: ../_static/connected.png diff --git a/docs/man/glances.1 b/docs/man/glances.1 index 4f2dd2ec..e8bc61ac 100644 --- a/docs/man/glances.1 +++ b/docs/man/glances.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "GLANCES" "1" "Feb 11, 2017" "2.8.3_DEVELOP" "Glances" +.TH "GLANCES" "1" "Feb 12, 2017" "2.8.3_DEVELOP" "Glances" .SH NAME glances \- An eye on your system . diff --git a/glances/main.py b/glances/main.py index cb18225e..4afd382f 100644 --- a/glances/main.py +++ b/glances/main.py @@ -103,6 +103,8 @@ Start the client browser (browser mode):\n\ dest='disable_alert', help='disable alert module') parser.add_argument('--disable-amps', action='store_true', default=False, dest='disable_amps', help='disable applications monitoring process (AMP) module') + parser.add_argument('--disable-cloud', action='store_true', default=False, + dest='disable_cloud', help='disable Cloud module') parser.add_argument('--disable-cpu', action='store_true', default=False, dest='disable_cpu', help='disable CPU module') parser.add_argument('--disable-diskio', action='store_true', default=False, diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 63bf15b2..edf89631 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -54,11 +54,12 @@ class _GlancesCurses(object): '3': {'switch': 'disable_quicklook'}, '6': {'switch': 'meangpu'}, '/': {'switch': 'process_short_name'}, - 'd': {'switch': 'disable_diskio'}, 'A': {'switch': 'disable_amps'}, 'b': {'switch': 'byte'}, 'B': {'switch': 'diskio_iops'}, + 'C': {'switch': 'disable_cloud'}, 'D': {'switch': 'disable_docker'}, + 'd': {'switch': 'disable_diskio'}, 'F': {'switch': 'fs_free_space'}, 'G': {'switch': 'disable_gpu'}, 'h': {'switch': 'help_tag'}, @@ -545,6 +546,7 @@ class _GlancesCurses(object): # ===================================== # Display first line (system+ip+uptime) + # Optionnaly: Cloud on second line # ===================================== self.__display_firstline(__stat_display) @@ -628,7 +630,11 @@ class _GlancesCurses(object): # Space between column self.space_between_column = 3 self.new_column() - self.display_plugin(stat_display["uptime"]) + self.display_plugin(stat_display["uptime"], + add_space=self.get_stats_display_width(stat_display["cloud"]) == 0) + self.init_column() + self.new_line() + self.display_plugin(stat_display["cloud"]) def __display_secondline(self, stat_display, stats): """Display the second line in the Curses interface. @@ -829,7 +835,8 @@ class _GlancesCurses(object): def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, - max_y=65535): + max_y=65535, + add_space=True): """Display the plugin_stats on the screen. If display_optional=True display the optional stats @@ -913,6 +920,10 @@ class _GlancesCurses(object): self.next_column, x_max + self.space_between_column) self.next_line = max(self.next_line, y + self.space_between_line) + if not add_space and self.next_line > 0: + # Do not have empty line after + self.next_line -= 1 + def erase(self): """Erase the content of the screen.""" self.term_window.erase() diff --git a/glances/plugins/glances_cloud.py b/glances/plugins/glances_cloud.py new file mode 100644 index 00000000..eb8d0560 --- /dev/null +++ b/glances/plugins/glances_cloud.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2017 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 . + +"""Cloud plugin. + +Supported Cloud API: +- AWS EC2 (class ThreadAwsEc2Grabber, see bellow) +""" + +try: + import requests +except ImportError: + cloud_tag = False +else: + cloud_tag = True + +import threading + +from glances.compat import iteritems +from glances.plugins.glances_plugin import GlancesPlugin +from glances.logger import logger + + +class Plugin(GlancesPlugin): + + """Glances' cloud plugin. + + The goal of this plugin is to retreive additional information + concerning the datacenter where the host is connected. + + See https://github.com/nicolargo/glances/issues/1029 + + stats is a dict + """ + + def __init__(self, args=None): + """Init the plugin.""" + super(Plugin, self).__init__(args=args) + + # We want to display the stat in the curse interface + self.display_curse = True + + # Init the stats + self.reset() + + # Init thread to grab AWS EC2 stats asynchroniously + self.aws_ec2 = ThreadAwsEc2Grabber() + + # Run the thread + self.aws_ec2. start() + + def reset(self): + """Reset/init the stats.""" + self.stats = {} + + def exit(self): + """Overwrite the exit method to close threads""" + self.aws_ec2.stop() + # Call the father class + super(Plugin, self).exit() + + @GlancesPlugin._check_decorator + @GlancesPlugin._log_result_decorator + def update(self): + """Update the cloud stats. + + Return the stats (dict) + """ + # Reset stats + self.reset() + + # Requests lib is needed to get stats from the Cloud API + if not cloud_tag: + return self.stats + + # Update the stats + if self.input_method == 'local': + self.stats = self.aws_ec2.stats + + return self.stats + + def msg_curse(self, args=None): + """Return the string to display in the curse interface.""" + # Init the return message + ret = [] + + if not self.stats \ + or self.stats == {} \ + or self.is_disable(): + return ret + + # Generate the output + if 'ami-id' in self.stats and 'region' in self.stats: + msg = 'AWS EC2' + ret.append(self.curse_add_line(msg, "TITLE")) + msg = ' instance {} ({})'.format(self.stats['ami-id'], + self.stats['region']) + ret.append(self.curse_add_line(msg)) + + # Return the message with decoration + logger.info(ret) + return ret + + +class ThreadAwsEc2Grabber(threading.Thread): + """ + Specific thread to grab AWS EC2 stats. + + stats is a dict + """ + + # AWS EC2 + # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html + AWS_EC2_API_URL = 'http://169.254.169.254/latest/meta-data' + AWS_EC2_API_METADATA = {'ami-id': 'ami-id', + 'region': 'placement/availability-zone'} + + def __init__(self): + """Init the class""" + logger.debug("cloud plugin - Create thread for AWS EC2") + super(ThreadAwsEc2Grabber, self).__init__() + # Event needed to stop properly the thread + self._stopper = threading.Event() + # The class return the stats as a dict + self._stats = {} + + def run(self): + """Function called to grab stats. + Infinite loop, should be stopped by calling the stop() method""" + + for k, v in iteritems(self.AWS_EC2_API_METADATA): + r_url = '{}/{}'.format(self.AWS_EC2_API_URL, v) + try: + r = requests.get(r_url) + except Exception as e: + logger.debug('Can not connect to the AWS EC2 API {}'.format(r_url, e)) + else: + self._stats[k] = r + + @property + def stats(self): + """Stats getter""" + return self._stats + + @stats.setter + def stats(self, value): + """Stats setter""" + self._stats = value + + def stop(self, timeout=None): + """Stop the thread""" + logger.debug("cloud plugin - Close thread for AWS EC2") + self._stopper.set() + + def stopped(self): + """Return True is the thread is stopped""" + return self._stopper.isSet()