1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of Glances. |
||
4 | # |
||
5 | # Copyright (C) 2019 Nicolargo <[email protected]> |
||
6 | # |
||
7 | # Glances is free software; you can redistribute it and/or modify |
||
8 | # it under the terms of the GNU Lesser General Public License as published by |
||
9 | # the Free Software Foundation, either version 3 of the License, or |
||
10 | # (at your option) any later version. |
||
11 | # |
||
12 | # Glances is distributed in the hope that it will be useful, |
||
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
15 | # GNU Lesser General Public License for more details. |
||
16 | # |
||
17 | # You should have received a copy of the GNU Lesser General Public License |
||
18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
19 | |||
20 | """Cloud plugin. |
||
21 | |||
22 | Supported Cloud API: |
||
23 | - OpenStack meta data (class ThreadOpenStack, see bellow): AWS, OVH... |
||
24 | """ |
||
25 | |||
26 | import threading |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
27 | |||
28 | from glances.compat import iteritems, to_ascii |
||
0 ignored issues
–
show
|
|||
29 | from glances.plugins.glances_plugin import GlancesPlugin |
||
0 ignored issues
–
show
|
|||
30 | from glances.logger import logger |
||
0 ignored issues
–
show
|
|||
31 | |||
32 | # Import plugin specific dependency |
||
33 | try: |
||
34 | import requests |
||
0 ignored issues
–
show
|
|||
35 | except ImportError as e: |
||
36 | import_error_tag = True |
||
37 | # Display debu message if import KeyError |
||
38 | logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e)) |
||
0 ignored issues
–
show
|
|||
39 | else: |
||
40 | import_error_tag = False |
||
41 | |||
42 | |||
43 | class Plugin(GlancesPlugin): |
||
44 | """Glances' cloud plugin. |
||
45 | |||
46 | The goal of this plugin is to retreive additional information |
||
47 | concerning the datacenter where the host is connected. |
||
48 | |||
49 | See https://github.com/nicolargo/glances/issues/1029 |
||
50 | |||
51 | stats is a dict |
||
52 | """ |
||
53 | |||
54 | def __init__(self, args=None, config=None): |
||
55 | """Init the plugin.""" |
||
56 | super(Plugin, self).__init__(args=args, config=config) |
||
57 | |||
58 | # We want to display the stat in the curse interface |
||
59 | self.display_curse = True |
||
60 | |||
61 | # Init the stats |
||
62 | self.reset() |
||
63 | |||
64 | # Init thread to grab OpenStack stats asynchroniously |
||
65 | self.OPENSTACK = ThreadOpenStack() |
||
66 | |||
67 | # Run the thread |
||
68 | self.OPENSTACK.start() |
||
69 | |||
70 | def exit(self): |
||
71 | """Overwrite the exit method to close threads.""" |
||
72 | self.OPENSTACK.stop() |
||
73 | # Call the father class |
||
74 | super(Plugin, self).exit() |
||
75 | |||
76 | @GlancesPlugin._check_decorator |
||
77 | @GlancesPlugin._log_result_decorator |
||
78 | def update(self): |
||
79 | """Update the cloud stats. |
||
80 | |||
81 | Return the stats (dict) |
||
82 | """ |
||
83 | # Init new stats |
||
84 | stats = self.get_init_value() |
||
85 | |||
86 | # Requests lib is needed to get stats from the Cloud API |
||
87 | if import_error_tag: |
||
88 | return stats |
||
89 | |||
90 | # Update the stats |
||
91 | if self.input_method == 'local': |
||
92 | stats = self.OPENSTACK.stats |
||
93 | # Example: |
||
94 | # Uncomment to test on physical computer |
||
95 | # stats = {'ami-id': 'ami-id', |
||
96 | # 'instance-id': 'instance-id', |
||
97 | # 'instance-type': 'instance-type', |
||
98 | # 'region': 'placement/availability-zone'} |
||
99 | |||
100 | # Update the stats |
||
101 | self.stats = stats |
||
102 | |||
103 | return self.stats |
||
104 | |||
105 | def msg_curse(self, args=None, max_width=None): |
||
106 | """Return the string to display in the curse interface.""" |
||
107 | # Init the return message |
||
108 | ret = [] |
||
109 | |||
110 | if not self.stats or self.stats == {} or self.is_disable(): |
||
111 | return ret |
||
112 | |||
113 | # Generate the output |
||
114 | if 'instance-type' in self.stats \ |
||
115 | and 'instance-id' in self.stats \ |
||
116 | and 'region' in self.stats: |
||
117 | msg = 'Cloud ' |
||
118 | ret.append(self.curse_add_line(msg, "TITLE")) |
||
119 | msg = '{} instance {} ({})'.format(self.stats['instance-type'], |
||
120 | self.stats['instance-id'], |
||
121 | self.stats['region']) |
||
122 | ret.append(self.curse_add_line(msg)) |
||
123 | |||
124 | # Return the message with decoration |
||
125 | # logger.info(ret) |
||
126 | return ret |
||
127 | |||
128 | |||
129 | class ThreadOpenStack(threading.Thread): |
||
130 | """ |
||
131 | Specific thread to grab OpenStack stats. |
||
132 | |||
133 | stats is a dict |
||
134 | """ |
||
135 | |||
136 | # https://docs.openstack.org/nova/latest/user/metadata-service.html |
||
137 | OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data' |
||
138 | OPENSTACK_API_METADATA = {'ami-id': 'ami-id', |
||
139 | 'instance-id': 'instance-id', |
||
140 | 'instance-type': 'instance-type', |
||
141 | 'region': 'placement/availability-zone'} |
||
142 | |||
143 | def __init__(self): |
||
144 | """Init the class.""" |
||
145 | logger.debug("cloud plugin - Create thread for OpenStack metadata") |
||
146 | super(ThreadOpenStack, self).__init__() |
||
147 | # Event needed to stop properly the thread |
||
148 | self._stopper = threading.Event() |
||
149 | # The class return the stats as a dict |
||
150 | self._stats = {} |
||
151 | |||
152 | def run(self): |
||
153 | """Grab plugin's stats. |
||
154 | |||
155 | Infinite loop, should be stopped by calling the stop() method |
||
156 | """ |
||
157 | if import_error_tag: |
||
158 | self.stop() |
||
159 | return False |
||
160 | |||
161 | for k, v in iteritems(self.OPENSTACK_API_METADATA): |
||
162 | r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v) |
||
163 | try: |
||
164 | # Local request, a timeout of 3 seconds is OK |
||
165 | r = requests.get(r_url, timeout=3) |
||
166 | except Exception as e: |
||
0 ignored issues
–
show
Catching very general exceptions such as
Exception is usually not recommended.
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed. So, unless you specifically plan to handle any error, consider adding a more specific exception.
Loading history...
|
|||
167 | logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e)) |
||
0 ignored issues
–
show
|
|||
168 | break |
||
169 | else: |
||
170 | if r.ok: |
||
171 | self._stats[k] = to_ascii(r.content) |
||
172 | |||
173 | return True |
||
174 | |||
175 | @property |
||
176 | def stats(self): |
||
177 | """Stats getter.""" |
||
178 | return self._stats |
||
179 | |||
180 | @stats.setter |
||
181 | def stats(self, value): |
||
182 | """Stats setter.""" |
||
183 | self._stats = value |
||
184 | |||
185 | def stop(self, timeout=None): |
||
0 ignored issues
–
show
|
|||
186 | """Stop the thread.""" |
||
187 | logger.debug("cloud plugin - Close thread for OpenStack metadata") |
||
188 | self._stopper.set() |
||
189 | |||
190 | def stopped(self): |
||
191 | """Return True is the thread is stopped.""" |
||
192 | return self._stopper.isSet() |
||
193 |