Completed
Push — master ( 2b80fa...6ea077 )
by Nicolas
01:22
created

GlancesBottle._api_config()   A

Complexity

Conditions 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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