Test Failed
Push — master ( 7e7379...128504 )
by Nicolas
03:31
created

GlancesBottle._api_all_limits()   A

Complexity

Conditions 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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