Test Failed
Push — develop ( 66c9ff...e21229 )
by Nicolas
05:06
created

glances/outputs/glances_bottle.py (5 issues)

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
"""Web interface class."""
21
22
import json
23
import os
24
import sys
25
import tempfile
0 ignored issues
show
import missing from __future__ import absolute_import
Loading history...
26
from io import open
27
import webbrowser
28
import zlib
29
30
from glances.compat import b
31
from glances.timer import Timer
32
from glances.logger import logger
33
34
try:
35
    from bottle import Bottle, static_file, abort, response, request, auth_basic, template, TEMPLATE_PATH
36
except ImportError:
37
    logger.critical('Bottle module not found. Glances cannot start in web server mode.')
38
    sys.exit(2)
39
40
41
def compress(func):
42
    """Compress result with deflate algorithm if the client ask for it."""
43
    def wrapper(*args, **kwargs):
44
        """Wrapper that take one function and return the compressed result."""
45
        ret = func(*args, **kwargs)
46
        logger.debug('Receive {} {} request with header: {}'.format(
47
            request.method,
48
            request.url,
49
            ['{}: {}'.format(h, request.headers.get(h)) for h in request.headers.keys()]
50
        ))
51
        if 'deflate' in request.headers.get('Accept-Encoding', ''):
52
            response.headers['Content-Encoding'] = 'deflate'
53
            ret = deflate_compress(ret)
54
        else:
55
            response.headers['Content-Encoding'] = 'identity'
56
        return ret
57
58
    def deflate_compress(data, compress_level=6):
59
        """Compress given data using the DEFLATE algorithm"""
60
        # Init compression
61
        zobj = zlib.compressobj(compress_level,
62
                                zlib.DEFLATED,
63
                                zlib.MAX_WBITS,
64
                                zlib.DEF_MEM_LEVEL,
65
                                zlib.Z_DEFAULT_STRATEGY)
66
67
        # Return compressed object
68
        return zobj.compress(b(data)) + zobj.flush()
69
70
    return wrapper
71
72
73
class GlancesBottle(object):
74
    """This class manages the Bottle Web server."""
75
76
    API_VERSION = '3'
77
78
    def __init__(self, config=None, args=None):
79
        # Init config
80
        self.config = config
81
82
        # Init args
83
        self.args = args
84
85
        # Init stats
86
        # Will be updated within Bottle route
87
        self.stats = None
88
89
        # cached_time is the minimum time interval between stats updates
90
        # i.e. HTTP/RESTful calls will not retrieve updated info until the time
91
        # since last update is passed (will retrieve old cached info instead)
92
        self.timer = Timer(0)
93
94
        # Load configuration file
95
        self.load_config(config)
96
97
        # Set the bind URL
98
        self.bind_url = 'http://{}:{}/'.format(self.args.bind_address,
99
                                               self.args.port)
100
101
        # Init Bottle
102
        self._app = Bottle()
103
        # Enable CORS (issue #479)
104
        self._app.install(EnableCors())
105
        # Password
106
        if args.password != '':
107
            self._app.install(auth_basic(self.check_auth))
108
        # Define routes
109
        self._route()
110
111
        # Path where the statics files are stored
112
        self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
0 ignored issues
show
This line is too long as per the coding-style (101/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
Coding Style Naming introduced by
The name STATIC_PATH does not conform to the attribute naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
113
114
        # Paths for templates
115
        TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates'))
116
117
    def load_config(self, config):
118
        """Load the outputs section of the configuration file."""
119
        # Limit the number of processes to display in the WebUI
120
        if config is not None and config.has_section('outputs'):
121
            logger.debug('Read number of processes to display in the WebUI')
122
            n = config.get_value('outputs', 'max_processes_display', default=None)
123
            logger.debug('Number of processes to display in the WebUI: {}'.format(n))
124
125
    def __update__(self):
126
        # Never update more than 1 time per cached_time
127
        if self.timer.finished():
128
            self.stats.update()
129
            self.timer = Timer(self.args.cached_time)
130
131
    def app(self):
132
        return self._app()
133
134
    def check_auth(self, username, password):
135
        """Check if a username/password combination is valid."""
136
        if username == self.args.username:
137
            from glances.password import GlancesPassword
138
            pwd = GlancesPassword()
139
            return pwd.check_password(self.args.password, pwd.sha256_hash(password))
140
        else:
141
            return False
142
143
    def _route(self):
144
        """Define route."""
145
        # REST API
146
        self._app.route('/api/%s/config' % self.API_VERSION, method="GET",
147
                        callback=self._api_config)
148
        self._app.route('/api/%s/config/<item>' % self.API_VERSION, method="GET",
149
                        callback=self._api_config_item)
150
        self._app.route('/api/%s/args' % self.API_VERSION, method="GET",
151
                        callback=self._api_args)
152
        self._app.route('/api/%s/args/<item>' % self.API_VERSION, method="GET",
153
                        callback=self._api_args_item)
154
        self._app.route('/api/%s/help' % self.API_VERSION, method="GET",
155
                        callback=self._api_help)
156
        self._app.route('/api/%s/pluginslist' % self.API_VERSION, method="GET",
157
                        callback=self._api_plugins)
158
        self._app.route('/api/%s/all' % self.API_VERSION, method="GET",
159
                        callback=self._api_all)
160
        self._app.route('/api/%s/all/limits' % self.API_VERSION, method="GET",
161
                        callback=self._api_all_limits)
162
        self._app.route('/api/%s/all/views' % self.API_VERSION, method="GET",
163
                        callback=self._api_all_views)
164
        self._app.route('/api/%s/<plugin>' % self.API_VERSION, method="GET",
165
                        callback=self._api)
166
        self._app.route('/api/%s/<plugin>/history' % self.API_VERSION, method="GET",
167
                        callback=self._api_history)
168
        self._app.route('/api/%s/<plugin>/history/<nb:int>' % self.API_VERSION, method="GET",
169
                        callback=self._api_history)
170
        self._app.route('/api/%s/<plugin>/limits' % self.API_VERSION, method="GET",
171
                        callback=self._api_limits)
172
        self._app.route('/api/%s/<plugin>/views' % self.API_VERSION, method="GET",
173
                        callback=self._api_views)
174
        self._app.route('/api/%s/<plugin>/<item>' % self.API_VERSION, method="GET",
175
                        callback=self._api_item)
176
        self._app.route('/api/%s/<plugin>/<item>/history' % self.API_VERSION, method="GET",
177
                        callback=self._api_item_history)
178
        self._app.route('/api/%s/<plugin>/<item>/history/<nb:int>' % self.API_VERSION, method="GET",
179
                        callback=self._api_item_history)
180
        self._app.route('/api/%s/<plugin>/<item>/<value>' % self.API_VERSION, method="GET",
181
                        callback=self._api_value)
182
        bindmsg = 'Glances RESTful API Server started on {}api/{}/'.format(self.bind_url,
183
                                                                           self.API_VERSION)
184
        logger.info(bindmsg)
185
186
        # WEB UI
187
        if not self.args.disable_webui:
188
            self._app.route('/', method="GET", callback=self._index)
189
            self._app.route('/<refresh_time:int>', method=["GET"], callback=self._index)
190
            self._app.route('/<filepath:path>', method="GET", callback=self._resource)
191
            bindmsg = 'Glances Web User Interface started on {}'.format(self.bind_url)
192
            logger.info(bindmsg)
193
        else:
194
            logger.info('The WebUI is disable (--disable-webui)')
195
196
        print(bindmsg)
197
198
    def start(self, stats):
199
        """Start the bottle."""
200
        # Init stats
201
        self.stats = stats
202
203
        # Init plugin list
204
        self.plugins_list = self.stats.getPluginsList()
205
206
        # Bind the Bottle TCP address/port
207
        if self.args.open_web_browser:
208
            # Implementation of the issue #946
209
            # Try to open the Glances Web UI in the default Web browser if:
210
            # 1) --open-web-browser option is used
211
            # 2) Glances standalone mode is running on Windows OS
212
            webbrowser.open(self.bind_url,
213
                            new=2,
214
                            autoraise=1)
215
216
        self._app.run(host=self.args.bind_address,
217
                      port=self.args.port,
218
                      quiet=not self.args.debug)
219
220
    def end(self):
221
        """End the bottle."""
222
        pass
223
224
    def _index(self, refresh_time=None):
225
        """Bottle callback for index.html (/) file."""
226
227
        if refresh_time is None or refresh_time < 1:
228
            refresh_time = self.args.time
229
230
        # Update the stat
231
        self.__update__()
232
233
        # Display
234
        return template("index.html", refresh_time=refresh_time)
235
236
    def _resource(self, filepath):
237
        """Bottle callback for resources files."""
238
        # Return the static file
239
        return static_file(filepath, root=self.STATIC_PATH)
240
241
    @compress
242
    def _api_help(self):
243
        """Glances API RESTful implementation.
244
245
        Return the help data or 404 error.
246
        """
247
        response.content_type = 'application/json; charset=utf-8'
248
249
        # Update the stat
250
        view_data = self.stats.get_plugin("help").get_view_data()
251
        try:
252
            plist = json.dumps(view_data, sort_keys=True)
253
        except Exception as e:
254
            abort(404, "Cannot get help view data (%s)" % str(e))
255
        return plist
256
257
    @compress
258
    def _api_plugins(self):
259
        """Glances API RESTFul implementation.
260
261
        @api {get} /api/%s/pluginslist Get plugins list
262
        @apiVersion 2.0
263
        @apiName pluginslist
264
        @apiGroup plugin
265
266
        @apiSuccess {String[]} Plugins list.
267
268
        @apiSuccessExample Success-Response:
269
            HTTP/1.1 200 OK
270
            [
271
               "load",
272
               "help",
273
               "ip",
274
               "memswap",
275
               "processlist",
276
               ...
277
            ]
278
279
         @apiError Cannot get plugin list.
280
281
         @apiErrorExample Error-Response:
282
            HTTP/1.1 404 Not Found
283
        """
284
        response.content_type = 'application/json; charset=utf-8'
285
286
        # Update the stat
287
        self.__update__()
288
289
        try:
290
            plist = json.dumps(self.plugins_list)
291
        except Exception as e:
292
            abort(404, "Cannot get plugin list (%s)" % str(e))
293
        return plist
294
295
    @compress
296
    def _api_all(self):
297
        """Glances API RESTful implementation.
298
299
        Return the JSON representation of all the plugins
300
        HTTP/200 if OK
301
        HTTP/400 if plugin is not found
302
        HTTP/404 if others error
303
        """
304
        response.content_type = 'application/json; charset=utf-8'
305
306
        if self.args.debug:
307
            fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
308
            try:
309
                with open(fname) as f:
310
                    return f.read()
311
            except IOError:
312
                logger.debug("Debug file (%s) not found" % fname)
313
314
        # Update the stat
315
        self.__update__()
316
317
        try:
318
            # Get the JSON value of the stat ID
319
            statval = json.dumps(self.stats.getAllAsDict())
320
        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...
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
321
            abort(404, "Cannot get stats (%s)" % str(e))
322
323
        return statval
324
325
    @compress
326
    def _api_all_limits(self):
327
        """Glances API RESTful implementation.
328
329
        Return the JSON representation of all the plugins limits
330
        HTTP/200 if OK
331
        HTTP/400 if plugin is not found
332
        HTTP/404 if others error
333
        """
334
        response.content_type = 'application/json; charset=utf-8'
335
336
        try:
337
            # Get the JSON value of the stat limits
338
            limits = json.dumps(self.stats.getAllLimitsAsDict())
339
        except Exception as e:
340
            abort(404, "Cannot get limits (%s)" % (str(e)))
341
        return limits
342
343
    @compress
344
    def _api_all_views(self):
345
        """Glances API RESTful implementation.
346
347
        Return the JSON representation of all the plugins views
348
        HTTP/200 if OK
349
        HTTP/400 if plugin is not found
350
        HTTP/404 if others error
351
        """
352
        response.content_type = 'application/json; charset=utf-8'
353
354
        try:
355
            # Get the JSON value of the stat view
356
            limits = json.dumps(self.stats.getAllViewsAsDict())
357
        except Exception as e:
358
            abort(404, "Cannot get views (%s)" % (str(e)))
359
        return limits
360
361
    @compress
362
    def _api(self, plugin):
363
        """Glances API RESTful implementation.
364
365
        Return the JSON representation of a given plugin
366
        HTTP/200 if OK
367
        HTTP/400 if plugin is not found
368
        HTTP/404 if others error
369
        """
370
        response.content_type = 'application/json; charset=utf-8'
371
372
        if plugin not in self.plugins_list:
373
            abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
374
375
        # Update the stat
376
        self.__update__()
377
378
        try:
379
            # Get the JSON value of the stat ID
380
            statval = self.stats.get_plugin(plugin).get_stats()
381
        except Exception as e:
382
            abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e)))
383
        return statval
384
385
    @compress
386
    def _api_history(self, plugin, nb=0):
387
        """Glances API RESTful implementation.
388
389
        Return the JSON representation of a given plugin history
390
        Limit to the last nb items (all if nb=0)
391
        HTTP/200 if OK
392
        HTTP/400 if plugin is not found
393
        HTTP/404 if others error
394
        """
395
        response.content_type = 'application/json; charset=utf-8'
396
397
        if plugin not in self.plugins_list:
398
            abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
399
400
        # Update the stat
401
        self.__update__()
402
403
        try:
404
            # Get the JSON value of the stat ID
405
            statval = self.stats.get_plugin(plugin).get_stats_history(nb=int(nb))
406
        except Exception as e:
407
            abort(404, "Cannot get plugin history %s (%s)" % (plugin, str(e)))
408
        return statval
409
410
    @compress
411
    def _api_limits(self, plugin):
412
        """Glances API RESTful implementation.
413
414
        Return the JSON limits of a given plugin
415
        HTTP/200 if OK
416
        HTTP/400 if plugin is not found
417
        HTTP/404 if others error
418
        """
419
        response.content_type = 'application/json; charset=utf-8'
420
421
        if plugin not in self.plugins_list:
422
            abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
423
424
        # Update the stat
425
        # self.__update__()
426
427
        try:
428
            # Get the JSON value of the stat limits
429
            ret = self.stats.get_plugin(plugin).limits
430
        except Exception as e:
431
            abort(404, "Cannot get limits for plugin %s (%s)" % (plugin, str(e)))
432
        return ret
433
434
    @compress
435
    def _api_views(self, plugin):
436
        """Glances API RESTful implementation.
437
438
        Return the JSON views of a given plugin
439
        HTTP/200 if OK
440
        HTTP/400 if plugin is not found
441
        HTTP/404 if others error
442
        """
443
        response.content_type = 'application/json; charset=utf-8'
444
445
        if plugin not in self.plugins_list:
446
            abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
447
448
        # Update the stat
449
        # self.__update__()
450
451
        try:
452
            # Get the JSON value of the stat views
453
            ret = self.stats.get_plugin(plugin).get_views()
454
        except Exception as e:
455
            abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e)))
456
        return ret
457
458
    @compress
459
    def _api_itemvalue(self, plugin, item, value=None, history=False, nb=0):
460
        """Father method for _api_item and _api_value."""
461
        response.content_type = 'application/json; charset=utf-8'
462
463
        if plugin not in self.plugins_list:
464
            abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
465
466
        # Update the stat
467
        self.__update__()
468
469
        if value is None:
470
            if history:
471
                ret = self.stats.get_plugin(plugin).get_stats_history(item, nb=int(nb))
472
            else:
473
                ret = self.stats.get_plugin(plugin).get_stats_item(item)
474
475
            if ret is None:
476
                abort(404, "Cannot get item %s%s in plugin %s" % (item, 'history ' if history else '', plugin))
477
        else:
478
            if history:
479
                # Not available
480
                ret = None
481
            else:
482
                ret = self.stats.get_plugin(plugin).get_stats_value(item, value)
483
484
            if ret is None:
485
                abort(404, "Cannot get item %s(%s=%s) in plugin %s" % ('history ' if history else '', item, value, plugin))
486
487
        return ret
488
489
    @compress
490
    def _api_item(self, plugin, item):
491
        """Glances API RESTful implementation.
492
493
        Return the JSON representation of the couple plugin/item
494
        HTTP/200 if OK
495
        HTTP/400 if plugin is not found
496
        HTTP/404 if others error
497
498
        """
499
        return self._api_itemvalue(plugin, item)
500
501
    @compress
502
    def _api_item_history(self, plugin, item, nb=0):
503
        """Glances API RESTful implementation.
504
505
        Return the JSON representation of the couple plugin/history of item
506
        HTTP/200 if OK
507
        HTTP/400 if plugin is not found
508
        HTTP/404 if others error
509
510
        """
511
        return self._api_itemvalue(plugin, item, history=True, nb=int(nb))
512
513
    @compress
514
    def _api_value(self, plugin, item, value):
515
        """Glances API RESTful implementation.
516
517
        Return the process stats (dict) for the given item=value
518
        HTTP/200 if OK
519
        HTTP/400 if plugin is not found
520
        HTTP/404 if others error
521
        """
522
        return self._api_itemvalue(plugin, item, value)
523
524
    @compress
525
    def _api_config(self):
526
        """Glances API RESTful implementation.
527
528
        Return the JSON representation of the Glances configuration file
529
        HTTP/200 if OK
530
        HTTP/404 if others error
531
        """
532
        response.content_type = 'application/json; charset=utf-8'
533
534
        try:
535
            # Get the JSON value of the config' dict
536
            args_json = json.dumps(self.config.as_dict())
537
        except Exception as e:
538
            abort(404, "Cannot get config (%s)" % str(e))
539
        return args_json
540
541
    @compress
542
    def _api_config_item(self, item):
543
        """Glances API RESTful implementation.
544
545
        Return the JSON representation of the Glances configuration item
546
        HTTP/200 if OK
547
        HTTP/400 if item is not found
548
        HTTP/404 if others error
549
        """
550
        response.content_type = 'application/json; charset=utf-8'
551
552
        config_dict = self.config.as_dict()
553
        if item not in config_dict:
554
            abort(400, "Unknown configuration item %s" % item)
555
556
        try:
557
            # Get the JSON value of the config' dict
558
            args_json = json.dumps(config_dict[item])
559
        except Exception as e:
560
            abort(404, "Cannot get config item (%s)" % str(e))
561
        return args_json
562
563
    @compress
564
    def _api_args(self):
565
        """Glances API RESTful implementation.
566
567
        Return the JSON representation of the Glances command line arguments
568
        HTTP/200 if OK
569
        HTTP/404 if others error
570
        """
571
        response.content_type = 'application/json; charset=utf-8'
572
573
        try:
574
            # Get the JSON value of the args' dict
575
            # Use vars to convert namespace to dict
576
            # Source: https://docs.python.org/%s/library/functions.html#vars
577
            args_json = json.dumps(vars(self.args))
578
        except Exception as e:
579
            abort(404, "Cannot get args (%s)" % str(e))
580
        return args_json
581
582
    @compress
583
    def _api_args_item(self, item):
584
        """Glances API RESTful implementation.
585
586
        Return the JSON representation of the Glances command line arguments item
587
        HTTP/200 if OK
588
        HTTP/400 if item is not found
589
        HTTP/404 if others error
590
        """
591
        response.content_type = 'application/json; charset=utf-8'
592
593
        if item not in self.args:
594
            abort(400, "Unknown argument item %s" % item)
595
596
        try:
597
            # Get the JSON value of the args' dict
598
            # Use vars to convert namespace to dict
599
            # Source: https://docs.python.org/%s/library/functions.html#vars
600
            args_json = json.dumps(vars(self.args)[item])
601
        except Exception as e:
602
            abort(404, "Cannot get args item (%s)" % str(e))
603
        return args_json
604
605
606
class EnableCors(object):
607
    name = 'enable_cors'
608
    api = 2
609
610
    def apply(self, fn, context):
611
        def _enable_cors(*args, **kwargs):
612
            # set CORS headers
613
            response.headers['Access-Control-Allow-Origin'] = '*'
614
            response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
615
            response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
616
617
            if request.method != 'OPTIONS':
618
                # actual request; reply with the actual response
619
                return fn(*args, **kwargs)
620
621
        return _enable_cors
622