1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of Glances. |
4
|
|
|
# |
5
|
|
|
# Copyright (C) 2015 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
|
|
|
"""Web interface class.""" |
21
|
|
|
|
22
|
|
|
import json |
23
|
|
|
import os |
24
|
|
|
import sys |
25
|
|
|
import tempfile |
26
|
|
|
from io import open |
27
|
|
|
|
28
|
|
|
from glances.logger import logger |
29
|
|
|
|
30
|
|
|
try: |
31
|
|
|
from bottle import Bottle, static_file, abort, response, request, auth_basic |
32
|
|
|
except ImportError: |
33
|
|
|
logger.critical('Bottle module not found. Glances cannot start in web server mode.') |
34
|
|
|
sys.exit(2) |
35
|
|
|
|
36
|
|
|
|
37
|
|
|
class GlancesBottle(object): |
38
|
|
|
|
39
|
|
|
"""This class manages the Bottle Web server.""" |
40
|
|
|
|
41
|
|
|
def __init__(self, args=None): |
42
|
|
|
# Init args |
43
|
|
|
self.args = args |
44
|
|
|
|
45
|
|
|
# Init stats |
46
|
|
|
# Will be updated within Bottle route |
47
|
|
|
self.stats = None |
48
|
|
|
|
49
|
|
|
# Init Bottle |
50
|
|
|
self._app = Bottle() |
51
|
|
|
# Enable CORS (issue #479) |
52
|
|
|
self._app.install(EnableCors()) |
53
|
|
|
# Password |
54
|
|
|
if args.password != '': |
55
|
|
|
self._app.install(auth_basic(self.check_auth)) |
56
|
|
|
# Define routes |
57
|
|
|
self._route() |
58
|
|
|
|
59
|
|
|
# Path where the statics files are stored |
60
|
|
|
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public') |
61
|
|
|
|
62
|
|
|
def app(self): |
63
|
|
|
return self._app() |
64
|
|
|
|
65
|
|
|
def check_auth(self, username, password): |
66
|
|
|
"""Check if a username/password combination is valid.""" |
67
|
|
|
if username == self.args.username: |
68
|
|
|
from glances.password import GlancesPassword |
69
|
|
|
pwd = GlancesPassword() |
70
|
|
|
return pwd.check_password(self.args.password, pwd.sha256_hash(password)) |
71
|
|
|
else: |
72
|
|
|
return False |
73
|
|
|
|
74
|
|
|
def _route(self): |
75
|
|
|
"""Define route.""" |
76
|
|
|
self._app.route('/', method="GET", callback=self._index) |
77
|
|
|
self._app.route('/<refresh_time:int>', method=["GET"], callback=self._index) |
78
|
|
|
|
79
|
|
|
# REST API |
80
|
|
|
self._app.route('/api/2/args', method="GET", callback=self._api_args) |
81
|
|
|
self._app.route('/api/2/args/<item>', method="GET", callback=self._api_args_item) |
82
|
|
|
self._app.route('/api/2/help', method="GET", callback=self._api_help) |
83
|
|
|
self._app.route('/api/2/pluginslist', method="GET", callback=self._api_plugins) |
84
|
|
|
self._app.route('/api/2/all', method="GET", callback=self._api_all) |
85
|
|
|
self._app.route('/api/2/all/limits', method="GET", callback=self._api_all_limits) |
86
|
|
|
self._app.route('/api/2/all/views', method="GET", callback=self._api_all_views) |
87
|
|
|
self._app.route('/api/2/<plugin>', method="GET", callback=self._api) |
88
|
|
|
self._app.route('/api/2/<plugin>/history', method="GET", callback=self._api_history) |
89
|
|
|
self._app.route('/api/2/<plugin>/history/<nb:int>', method="GET", callback=self._api_history) |
90
|
|
|
self._app.route('/api/2/<plugin>/limits', method="GET", callback=self._api_limits) |
91
|
|
|
self._app.route('/api/2/<plugin>/views', method="GET", callback=self._api_views) |
92
|
|
|
self._app.route('/api/2/<plugin>/<item>', method="GET", callback=self._api_item) |
93
|
|
|
self._app.route('/api/2/<plugin>/<item>/history', method="GET", callback=self._api_item_history) |
94
|
|
|
self._app.route('/api/2/<plugin>/<item>/history/<nb:int>', method="GET", callback=self._api_item_history) |
95
|
|
|
self._app.route('/api/2/<plugin>/<item>/<value>', method="GET", callback=self._api_value) |
96
|
|
|
|
97
|
|
|
self._app.route('/<filepath:path>', method="GET", callback=self._resource) |
98
|
|
|
|
99
|
|
|
def start(self, stats): |
100
|
|
|
"""Start the bottle.""" |
101
|
|
|
# Init stats |
102
|
|
|
self.stats = stats |
103
|
|
|
|
104
|
|
|
# Init plugin list |
105
|
|
|
self.plugins_list = self.stats.getAllPlugins() |
106
|
|
|
|
107
|
|
|
# Bind the Bottle TCP address/port |
108
|
|
|
bindmsg = 'Glances web server started on http://{}:{}/'.format(self.args.bind_address, self.args.port) |
109
|
|
|
logger.info(bindmsg) |
110
|
|
|
print(bindmsg) |
111
|
|
|
self._app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug) |
112
|
|
|
|
113
|
|
|
def end(self): |
114
|
|
|
"""End the bottle.""" |
115
|
|
|
pass |
116
|
|
|
|
117
|
|
|
def _index(self, refresh_time=None): |
118
|
|
|
"""Bottle callback for index.html (/) file.""" |
119
|
|
|
# Update the stat |
120
|
|
|
self.stats.update() |
121
|
|
|
|
122
|
|
|
# Display |
123
|
|
|
return static_file("index.html", root=self.STATIC_PATH) |
124
|
|
|
|
125
|
|
|
def _resource(self, filepath): |
126
|
|
|
"""Bottle callback for resources files.""" |
127
|
|
|
# Return the static file |
128
|
|
|
return static_file(filepath, root=self.STATIC_PATH) |
129
|
|
|
|
130
|
|
|
def _api_help(self): |
131
|
|
|
"""Glances API RESTFul implementation. |
132
|
|
|
|
133
|
|
|
Return the help data or 404 error. |
134
|
|
|
""" |
135
|
|
|
response.content_type = 'application/json' |
136
|
|
|
|
137
|
|
|
# Update the stat |
138
|
|
|
view_data = self.stats.get_plugin("help").get_view_data() |
139
|
|
|
try: |
140
|
|
|
plist = json.dumps(view_data, sort_keys=True) |
141
|
|
|
except Exception as e: |
142
|
|
|
abort(404, "Cannot get help view data (%s)" % str(e)) |
143
|
|
|
return plist |
144
|
|
|
|
145
|
|
|
def _api_plugins(self): |
146
|
|
|
""" |
147
|
|
|
@api {get} /api/2/pluginslist Get plugins list |
148
|
|
|
@apiVersion 2.0 |
149
|
|
|
@apiName pluginslist |
150
|
|
|
@apiGroup plugin |
151
|
|
|
|
152
|
|
|
@apiSuccess {String[]} Plugins list. |
153
|
|
|
|
154
|
|
|
@apiSuccessExample Success-Response: |
155
|
|
|
HTTP/1.1 200 OK |
156
|
|
|
[ |
157
|
|
|
"load", |
158
|
|
|
"help", |
159
|
|
|
"ip", |
160
|
|
|
"memswap", |
161
|
|
|
"processlist", |
162
|
|
|
... |
163
|
|
|
] |
164
|
|
|
|
165
|
|
|
@apiError Cannot get plugin list. |
166
|
|
|
|
167
|
|
|
@apiErrorExample Error-Response: |
168
|
|
|
HTTP/1.1 404 Not Found |
169
|
|
|
""" |
170
|
|
|
response.content_type = 'application/json' |
171
|
|
|
|
172
|
|
|
# Update the stat |
173
|
|
|
self.stats.update() |
174
|
|
|
|
175
|
|
|
try: |
176
|
|
|
plist = json.dumps(self.plugins_list) |
177
|
|
|
except Exception as e: |
178
|
|
|
abort(404, "Cannot get plugin list (%s)" % str(e)) |
179
|
|
|
return plist |
180
|
|
|
|
181
|
|
|
def _api_all(self): |
182
|
|
|
"""Glances API RESTFul implementation. |
183
|
|
|
|
184
|
|
|
Return the JSON representation of all the plugins |
185
|
|
|
HTTP/200 if OK |
186
|
|
|
HTTP/400 if plugin is not found |
187
|
|
|
HTTP/404 if others error |
188
|
|
|
""" |
189
|
|
|
response.content_type = 'application/json' |
190
|
|
|
|
191
|
|
|
if self.args.debug: |
192
|
|
|
fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json') |
193
|
|
|
try: |
194
|
|
|
with open(fname) as f: |
195
|
|
|
return f.read() |
196
|
|
|
except IOError: |
197
|
|
|
logger.debug("Debug file (%s) not found" % fname) |
198
|
|
|
|
199
|
|
|
# Update the stat |
200
|
|
|
self.stats.update() |
201
|
|
|
|
202
|
|
|
try: |
203
|
|
|
# Get the JSON value of the stat ID |
204
|
|
|
statval = json.dumps(self.stats.getAllAsDict()) |
205
|
|
|
except Exception as e: |
206
|
|
|
abort(404, "Cannot get stats (%s)" % str(e)) |
207
|
|
|
return statval |
208
|
|
|
|
209
|
|
|
def _api_all_limits(self): |
210
|
|
|
"""Glances API RESTFul implementation. |
211
|
|
|
|
212
|
|
|
Return the JSON representation of all the plugins limits |
213
|
|
|
HTTP/200 if OK |
214
|
|
|
HTTP/400 if plugin is not found |
215
|
|
|
HTTP/404 if others error |
216
|
|
|
""" |
217
|
|
|
response.content_type = 'application/json' |
218
|
|
|
|
219
|
|
|
try: |
220
|
|
|
# Get the JSON value of the stat limits |
221
|
|
|
limits = json.dumps(self.stats.getAllLimitsAsDict()) |
222
|
|
|
except Exception as e: |
223
|
|
|
abort(404, "Cannot get limits (%s)" % (str(e))) |
224
|
|
|
return limits |
225
|
|
|
|
226
|
|
|
def _api_all_views(self): |
227
|
|
|
"""Glances API RESTFul implementation. |
228
|
|
|
|
229
|
|
|
Return the JSON representation of all the plugins views |
230
|
|
|
HTTP/200 if OK |
231
|
|
|
HTTP/400 if plugin is not found |
232
|
|
|
HTTP/404 if others error |
233
|
|
|
""" |
234
|
|
|
response.content_type = 'application/json' |
235
|
|
|
|
236
|
|
|
try: |
237
|
|
|
# Get the JSON value of the stat view |
238
|
|
|
limits = json.dumps(self.stats.getAllViewsAsDict()) |
239
|
|
|
except Exception as e: |
240
|
|
|
abort(404, "Cannot get views (%s)" % (str(e))) |
241
|
|
|
return limits |
242
|
|
|
|
243
|
|
|
def _api(self, plugin): |
244
|
|
|
"""Glances API RESTFul implementation. |
245
|
|
|
|
246
|
|
|
Return the JSON representation of a given plugin |
247
|
|
|
HTTP/200 if OK |
248
|
|
|
HTTP/400 if plugin is not found |
249
|
|
|
HTTP/404 if others error |
250
|
|
|
""" |
251
|
|
|
response.content_type = 'application/json' |
252
|
|
|
|
253
|
|
|
if plugin not in self.plugins_list: |
254
|
|
|
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) |
255
|
|
|
|
256
|
|
|
# Update the stat |
257
|
|
|
self.stats.update() |
258
|
|
|
|
259
|
|
|
try: |
260
|
|
|
# Get the JSON value of the stat ID |
261
|
|
|
statval = self.stats.get_plugin(plugin).get_stats() |
262
|
|
|
except Exception as e: |
263
|
|
|
abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e))) |
264
|
|
|
return statval |
265
|
|
|
|
266
|
|
|
def _api_history(self, plugin, nb=0): |
267
|
|
|
"""Glances API RESTFul implementation. |
268
|
|
|
|
269
|
|
|
Return the JSON representation of a given plugin history |
270
|
|
|
Limit to the last nb items (all if nb=0) |
271
|
|
|
HTTP/200 if OK |
272
|
|
|
HTTP/400 if plugin is not found |
273
|
|
|
HTTP/404 if others error |
274
|
|
|
""" |
275
|
|
|
response.content_type = 'application/json' |
276
|
|
|
|
277
|
|
|
if plugin not in self.plugins_list: |
278
|
|
|
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) |
279
|
|
|
|
280
|
|
|
# Update the stat |
281
|
|
|
self.stats.update() |
282
|
|
|
|
283
|
|
|
try: |
284
|
|
|
# Get the JSON value of the stat ID |
285
|
|
|
statval = self.stats.get_plugin(plugin).get_stats_history(nb=int(nb)) |
286
|
|
|
except Exception as e: |
287
|
|
|
abort(404, "Cannot get plugin history %s (%s)" % (plugin, str(e))) |
288
|
|
|
return statval |
289
|
|
|
|
290
|
|
|
def _api_limits(self, plugin): |
291
|
|
|
"""Glances API RESTFul implementation. |
292
|
|
|
|
293
|
|
|
Return the JSON limits of a given plugin |
294
|
|
|
HTTP/200 if OK |
295
|
|
|
HTTP/400 if plugin is not found |
296
|
|
|
HTTP/404 if others error |
297
|
|
|
""" |
298
|
|
|
response.content_type = 'application/json' |
299
|
|
|
|
300
|
|
|
if plugin not in self.plugins_list: |
301
|
|
|
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) |
302
|
|
|
|
303
|
|
|
# Update the stat |
304
|
|
|
# self.stats.update() |
305
|
|
|
|
306
|
|
|
try: |
307
|
|
|
# Get the JSON value of the stat limits |
308
|
|
|
ret = self.stats.get_plugin(plugin).limits |
309
|
|
|
except Exception as e: |
310
|
|
|
abort(404, "Cannot get limits for plugin %s (%s)" % (plugin, str(e))) |
311
|
|
|
return ret |
312
|
|
|
|
313
|
|
|
def _api_views(self, plugin): |
314
|
|
|
"""Glances API RESTFul implementation. |
315
|
|
|
|
316
|
|
|
Return the JSON views of a given plugin |
317
|
|
|
HTTP/200 if OK |
318
|
|
|
HTTP/400 if plugin is not found |
319
|
|
|
HTTP/404 if others error |
320
|
|
|
""" |
321
|
|
|
response.content_type = 'application/json' |
322
|
|
|
|
323
|
|
|
if plugin not in self.plugins_list: |
324
|
|
|
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) |
325
|
|
|
|
326
|
|
|
# Update the stat |
327
|
|
|
# self.stats.update() |
328
|
|
|
|
329
|
|
|
try: |
330
|
|
|
# Get the JSON value of the stat views |
331
|
|
|
ret = self.stats.get_plugin(plugin).get_views() |
332
|
|
|
except Exception as e: |
333
|
|
|
abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e))) |
334
|
|
|
return ret |
335
|
|
|
|
336
|
|
|
def _api_itemvalue(self, plugin, item, value=None, history=False, nb=0): |
337
|
|
|
"""Father method for _api_item and _api_value""" |
338
|
|
|
response.content_type = 'application/json' |
339
|
|
|
|
340
|
|
|
if plugin not in self.plugins_list: |
341
|
|
|
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) |
342
|
|
|
|
343
|
|
|
# Update the stat |
344
|
|
|
self.stats.update() |
345
|
|
|
|
346
|
|
|
if value is None: |
347
|
|
|
if history: |
348
|
|
|
ret = self.stats.get_plugin(plugin).get_stats_history(item, nb=int(nb)) |
349
|
|
|
else: |
350
|
|
|
ret = self.stats.get_plugin(plugin).get_stats_item(item) |
351
|
|
|
|
352
|
|
|
if ret is None: |
353
|
|
|
abort(404, "Cannot get item %s%s in plugin %s" % (item, 'history ' if history else '', plugin)) |
354
|
|
|
else: |
355
|
|
|
if history: |
356
|
|
|
# Not available |
357
|
|
|
ret = None |
358
|
|
|
else: |
359
|
|
|
ret = self.stats.get_plugin(plugin).get_stats_value(item, value) |
360
|
|
|
|
361
|
|
|
if ret is None: |
362
|
|
|
abort(404, "Cannot get item %s(%s=%s) in plugin %s" % ('history ' if history else '', item, value, plugin)) |
363
|
|
|
|
364
|
|
|
return ret |
365
|
|
|
|
366
|
|
|
def _api_item(self, plugin, item): |
367
|
|
|
"""Glances API RESTFul implementation. |
368
|
|
|
|
369
|
|
|
Return the JSON representation of the couple plugin/item |
370
|
|
|
HTTP/200 if OK |
371
|
|
|
HTTP/400 if plugin is not found |
372
|
|
|
HTTP/404 if others error |
373
|
|
|
|
374
|
|
|
""" |
375
|
|
|
return self._api_itemvalue(plugin, item) |
376
|
|
|
|
377
|
|
|
def _api_item_history(self, plugin, item, nb=0): |
378
|
|
|
"""Glances API RESTFul implementation. |
379
|
|
|
|
380
|
|
|
Return the JSON representation of the couple plugin/history of item |
381
|
|
|
HTTP/200 if OK |
382
|
|
|
HTTP/400 if plugin is not found |
383
|
|
|
HTTP/404 if others error |
384
|
|
|
|
385
|
|
|
""" |
386
|
|
|
return self._api_itemvalue(plugin, item, history=True, nb=int(nb)) |
387
|
|
|
|
388
|
|
|
def _api_value(self, plugin, item, value): |
389
|
|
|
"""Glances API RESTFul implementation. |
390
|
|
|
|
391
|
|
|
Return the process stats (dict) for the given item=value |
392
|
|
|
HTTP/200 if OK |
393
|
|
|
HTTP/400 if plugin is not found |
394
|
|
|
HTTP/404 if others error |
395
|
|
|
""" |
396
|
|
|
return self._api_itemvalue(plugin, item, value) |
397
|
|
|
|
398
|
|
|
def _api_args(self): |
399
|
|
|
"""Glances API RESTFul implementation. |
400
|
|
|
|
401
|
|
|
Return the JSON representation of the Glances command line arguments |
402
|
|
|
HTTP/200 if OK |
403
|
|
|
HTTP/404 if others error |
404
|
|
|
""" |
405
|
|
|
response.content_type = 'application/json' |
406
|
|
|
|
407
|
|
|
try: |
408
|
|
|
# Get the JSON value of the args' dict |
409
|
|
|
# Use vars to convert namespace to dict |
410
|
|
|
# Source: https://docs.python.org/2/library/functions.html#vars |
411
|
|
|
args_json = json.dumps(vars(self.args)) |
412
|
|
|
except Exception as e: |
413
|
|
|
abort(404, "Cannot get args (%s)" % str(e)) |
414
|
|
|
return args_json |
415
|
|
|
|
416
|
|
|
def _api_args_item(self, item): |
417
|
|
|
"""Glances API RESTFul implementation. |
418
|
|
|
|
419
|
|
|
Return the JSON representation of the Glances command line arguments item |
420
|
|
|
HTTP/200 if OK |
421
|
|
|
HTTP/400 if item is not found |
422
|
|
|
HTTP/404 if others error |
423
|
|
|
""" |
424
|
|
|
response.content_type = 'application/json' |
425
|
|
|
|
426
|
|
|
if item not in self.args: |
427
|
|
|
abort(400, "Unknown item %s" % item) |
428
|
|
|
|
429
|
|
|
try: |
430
|
|
|
# Get the JSON value of the args' dict |
431
|
|
|
# Use vars to convert namespace to dict |
432
|
|
|
# Source: https://docs.python.org/2/library/functions.html#vars |
433
|
|
|
args_json = json.dumps(vars(self.args)[item]) |
434
|
|
|
except Exception as e: |
435
|
|
|
abort(404, "Cannot get args item (%s)" % str(e)) |
436
|
|
|
return args_json |
437
|
|
|
|
438
|
|
|
|
439
|
|
|
class EnableCors(object): |
440
|
|
|
name = 'enable_cors' |
441
|
|
|
api = 2 |
442
|
|
|
|
443
|
|
|
def apply(self, fn, context): |
444
|
|
|
def _enable_cors(*args, **kwargs): |
445
|
|
|
# set CORS headers |
446
|
|
|
response.headers['Access-Control-Allow-Origin'] = '*' |
447
|
|
|
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS' |
448
|
|
|
response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' |
449
|
|
|
|
450
|
|
|
if request.method != 'OPTIONS': |
451
|
|
|
# actual request; reply with the actual response |
452
|
|
|
return fn(*args, **kwargs) |
453
|
|
|
|
454
|
|
|
return _enable_cors |
455
|
|
|
|