Completed
Pull Request — master (#963)
by
unknown
01:02
created

GlancesBottle.__init__()   B

Complexity

Conditions 2

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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