GlancesRestfulApi._api_key()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nop 4
dl 0
loc 23
rs 9.9
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""RestFull API interface class."""
10
11
import os
12
import socket
13
import sys
14
import tempfile
15
import webbrowser
16
from typing import Annotated, Any, Union
17
from urllib.parse import urljoin
18
19
from glances import __apiversion__, __version__
20
from glances.events_list import glances_events
21
from glances.globals import json_dumps, weak_lru_cache
22
from glances.logger import logger
23
from glances.password import GlancesPassword
24
from glances.processes import glances_processes
25
from glances.servers_list import GlancesServersList
26
from glances.servers_list_dynamic import GlancesAutoDiscoverClient
27
from glances.stats import GlancesStats
28
from glances.timer import Timer
29
30
# FastAPI import
31
try:
32
    from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, status
33
    from fastapi.middleware.cors import CORSMiddleware
34
    from fastapi.middleware.gzip import GZipMiddleware
35
    from fastapi.responses import HTMLResponse, JSONResponse
36
    from fastapi.security import HTTPBasic, HTTPBasicCredentials
37
    from fastapi.staticfiles import StaticFiles
38
    from fastapi.templating import Jinja2Templates
39
except ImportError as e:
40
    logger.critical(f'FastAPI import error: {e}')
41
    logger.critical('Glances cannot start in web server mode.')
42
    sys.exit(2)
43
44
try:
45
    import uvicorn
46
except ImportError:
47
    logger.critical('Uvicorn import error. Glances cannot start in web server mode.')
48
    sys.exit(2)
49
import builtins
50
import contextlib
51
import threading
52
import time
53
54
security = HTTPBasic()
55
56
57
class GlancesJSONResponse(JSONResponse):
58
    """
59
    Glances impl of fastapi's JSONResponse to use internal JSON Serialization features
60
61
    Ref: https://fastapi.tiangolo.com/advanced/custom-response/
62
    """
63
64
    def render(self, content: Any) -> bytes:
65
        return json_dumps(content)
66
67
68
class GlancesUvicornServer(uvicorn.Server):
69
    def install_signal_handlers(self):
70
        pass
71
72
    @contextlib.contextmanager
73
    def run_in_thread(self, timeout=3):
74
        thread = threading.Thread(target=self.run)
75
        thread.start()
76
        try:
77
            chrono = Timer(timeout)
78
            while not self.started and not chrono.finished():
79
                time.sleep(0.5)
80
            # Timeout reached
81
            # Something go wrong...
82
            # The Uvicorn server should be stopped
83
            if not self.started:
84
                self.should_exit = True
85
                thread.join()
86
            yield
87
        finally:
88
            self.should_exit = True
89
            thread.join()
90
91
92
class GlancesRestfulApi:
93
    """This class manages the Restful API server."""
94
95
    API_VERSION = __apiversion__
96
97
    def __init__(self, config=None, args=None):
98
        # Init config
99
        self.config = config
100
101
        # Init args
102
        self.args = args
103
104
        # Init stats
105
        # Will be updated within route
106
        self.stats = None
107
108
        # Init servers list (only for the browser mode)
109
        if self.args.browser:
110
            self.servers_list = GlancesServersList(config=config, args=args)
111
        else:
112
            self.servers_list = None
113
114
        # cached_time is the minimum time interval between stats updates
115
        # i.e. HTTP/RESTful calls will not retrieve updated info until the time
116
        # since last update is passed (will retrieve old cached info instead)
117
        self.timer = Timer(0)
118
119
        # Load configuration file
120
        self.load_config(config)
121
122
        # Set the bind URL
123
        self.bind_url = urljoin(f'http://{self.args.bind_address}:{self.args.port}/', self.url_prefix)
124
125
        # FastAPI Init
126
        if self.args.password:
127
            self._app = FastAPI(default_response_class=GlancesJSONResponse, dependencies=[Depends(self.authentication)])
128
            self._password = GlancesPassword(username=args.username, config=config)
129
130
        else:
131
            self._app = FastAPI(default_response_class=GlancesJSONResponse)
132
            self._password = None
133
134
        # Set path for WebUI
135
        webui_root_path = config.get_value(
136
            'outputs', 'webui_root_path', default=os.path.dirname(os.path.realpath(__file__))
137
        )
138
        if webui_root_path == '':
139
            webui_root_path = os.path.dirname(os.path.realpath(__file__))
140
        self.STATIC_PATH = os.path.join(webui_root_path, 'static/public')
141
        self.TEMPLATE_PATH = os.path.join(webui_root_path, 'static/templates')
142
        self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
143
144
        # FastAPI Enable GZIP compression
145
        # https://fastapi.tiangolo.com/advanced/middleware/
146
        # Should be done before other middlewares to avoid
147
        # LocalProtocolError("Too much data for declared Content-Length")
148
        self._app.add_middleware(GZipMiddleware, minimum_size=1000)
149
150
        # FastAPI Enable CORS
151
        # https://fastapi.tiangolo.com/tutorial/cors/
152
        self._app.add_middleware(
153
            CORSMiddleware,
154
            # Related to https://github.com/nicolargo/glances/issues/2812
155
            allow_origins=config.get_list_value('outputs', 'cors_origins', default=["*"]),
156
            allow_credentials=config.get_bool_value('outputs', 'cors_credentials', default=True),
157
            allow_methods=config.get_list_value('outputs', 'cors_methods', default=["*"]),
158
            allow_headers=config.get_list_value('outputs', 'cors_headers', default=["*"]),
159
        )
160
161
        # FastAPI Define routes
162
        self._app.include_router(self._router())
163
164
        # Enable auto discovering of the service
165
        self.autodiscover_client = None
166
        if not self.args.disable_autodiscover:
167
            logger.info('Autodiscover is enabled with service name {}'.format(socket.gethostname().split('.', 1)[0]))
168
            self.autodiscover_client = GlancesAutoDiscoverClient(socket.gethostname().split('.', 1)[0], self.args)
169
        else:
170
            logger.info("Glances autodiscover announce is disabled")
171
172
    def load_config(self, config):
173
        """Load the outputs section of the configuration file."""
174
        # Limit the number of processes to display in the WebUI
175
        self.url_prefix = ''
176
        if config is not None and config.has_section('outputs'):
177
            # Max process to display in the WebUI
178
            n = config.get_value('outputs', 'max_processes_display', default=None)
179
            logger.debug(f'Number of processes to display in the WebUI: {n}')
180
            # URL prefix
181
            self.url_prefix = config.get_value('outputs', 'url_prefix', default='')
182
            if self.url_prefix != '':
183
                self.url_prefix = self.url_prefix.rstrip('/')
184
            logger.debug(f'URL prefix: {self.url_prefix}')
185
186
    def __update_stats(self):
187
        # Never update more than 1 time per cached_time
188
        if self.timer.finished():
189
            self.stats.update()
190
            self.timer = Timer(self.args.cached_time)
191
192
    def __update_servers_list(self):
193
        # Never update more than 1 time per cached_time
194
        if self.timer.finished() and self.servers_list is not None:
195
            self.servers_list.update_servers_stats()
196
            self.timer = Timer(self.args.cached_time)
197
198
    def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
199
        """Check if a username/password combination is valid."""
200
        if creds.username == self.args.username:
201
            # check_password
202
            if self._password.check_password(self.args.password, self._password.get_hash(creds.password)):
203
                return creds.username
204
205
        # If the username/password combination is invalid, return an HTTP 401
206
        raise HTTPException(
207
            status.HTTP_401_UNAUTHORIZED, "Incorrect username or password", {"WWW-Authenticate": "Basic"}
208
        )
209
210
    def _router(self) -> APIRouter:
211
        """Define a custom router for Glances path."""
212
        base_path = f'/api/{self.API_VERSION}'
213
        plugin_path = f"{base_path}/{{plugin}}"
214
215
        # Create the main router
216
        router = APIRouter(prefix=self.url_prefix)
217
218
        # REST API route definition
219
        # ==========================
220
221
        # HEAD
222
        router.add_api_route(f'{base_path}/status', self._api_status, methods=['HEAD', 'GET'])
223
224
        # POST
225
        router.add_api_route(f'{base_path}/events/clear/warning', self._events_clear_warning, methods=['POST'])
226
        router.add_api_route(f'{base_path}/events/clear/all', self._events_clear_all, methods=['POST'])
227
        router.add_api_route(
228
            f'{base_path}/processes/extended/disable', self._api_disable_extended_processes, methods=['POST']
229
        )
230
        router.add_api_route(
231
            f'{base_path}/processes/extended/{{pid}}', self._api_set_extended_processes, methods=['POST']
232
        )
233
234
        # GET
235
        route_mapping = {
236
            f'{base_path}/config': self._api_config,
237
            f'{base_path}/config/{{section}}': self._api_config_section,
238
            f'{base_path}/config/{{section}}/{{item}}': self._api_config_section_item,
239
            f'{base_path}/args': self._api_args,
240
            f'{base_path}/args/{{item}}': self._api_args_item,
241
            f'{base_path}/help': self._api_help,
242
            f'{base_path}/all': self._api_all,
243
            f'{base_path}/all/limits': self._api_all_limits,
244
            f'{base_path}/all/views': self._api_all_views,
245
            f'{base_path}/pluginslist': self._api_plugins,
246
            f'{base_path}/serverslist': self._api_servers_list,
247
            f'{base_path}/processes/extended': self._api_get_extended_processes,
248
            f'{base_path}/processes/{{pid}}': self._api_get_processes,
249
            f'{plugin_path}': self._api,
250
            f'{plugin_path}/history': self._api_history,
251
            f'{plugin_path}/history/{{nb}}': self._api_history,
252
            f'{plugin_path}/top/{{nb}}': self._api_top,
253
            f'{plugin_path}/limits': self._api_limits,
254
            f'{plugin_path}/views': self._api_views,
255
            f'{plugin_path}/{{item}}': self._api_item,
256
            f'{plugin_path}/{{item}}/views': self._api_item_views,
257
            f'{plugin_path}/{{item}}/history': self._api_item_history,
258
            f'{plugin_path}/{{item}}/history/{{nb}}': self._api_item_history,
259
            f'{plugin_path}/{{item}}/description': self._api_item_description,
260
            f'{plugin_path}/{{item}}/unit': self._api_item_unit,
261
            f'{plugin_path}/{{item}}/value/{{value:path}}': self._api_value,
262
            f'{plugin_path}/{{item}}/{{key}}': self._api_key,
263
            f'{plugin_path}/{{item}}/{{key}}/views': self._api_key_views,
264
        }
265
        for path, endpoint in route_mapping.items():
266
            router.add_api_route(path, endpoint)
267
268
        # Browser WEBUI
269
        if self.args.browser:
270
            # Template for the root browser.html file
271
            router.add_api_route('/browser', self._browser, response_class=HTMLResponse)
272
273
            # Statics files
274
            self._app.mount(self.url_prefix + '/static', StaticFiles(directory=self.STATIC_PATH), name="static")
275
            logger.debug(f"The Browser WebUI is enable and got statics files in {self.STATIC_PATH}")
276
277
            bindmsg = f'Glances Browser Web User Interface started on {self.bind_url}browser'
278
            logger.info(bindmsg)
279
            print(bindmsg)
280
281
        # WEBUI
282
        if not self.args.disable_webui:
283
            # Template for the root index.html file
284
            router.add_api_route('/', self._index, response_class=HTMLResponse)
285
286
            # Statics files
287
            self._app.mount(self.url_prefix + '/static', StaticFiles(directory=self.STATIC_PATH), name="static")
288
            logger.debug(f"The WebUI is enable and got statics files in {self.STATIC_PATH}")
289
290
            bindmsg = f'Glances Web User Interface started on {self.bind_url}'
291
            logger.info(bindmsg)
292
            print(bindmsg)
293
        else:
294
            logger.info('The WebUI is disable (--disable-webui)')
295
296
        # Restful API
297
        bindmsg = f'Glances RESTful API Server started on {self.bind_url}api/{self.API_VERSION}'
298
        logger.info(bindmsg)
299
        print(bindmsg)
300
301
        return router
302
303
    def start(self, stats: GlancesStats) -> None:
304
        """Start the bottle."""
305
        # Init stats
306
        self.stats = stats
307
308
        # Init plugin list
309
        self.plugins_list = self.stats.getPluginsList()
310
311
        if self.args.open_web_browser:
312
            # Implementation of the issue #946
313
            # Try to open the Glances Web UI in the default Web browser if:
314
            # 1) --open-web-browser option is used
315
            # 2) Glances standalone mode is running on Windows OS
316
            webbrowser.open(self.bind_url, new=2, autoraise=1)
317
318
        # Start Uvicorn server
319
        self._start_uvicorn()
320
321
    def _start_uvicorn(self):
322
        # Run the Uvicorn Web server
323
        uvicorn_config = uvicorn.Config(
324
            self._app, host=self.args.bind_address, port=self.args.port, access_log=self.args.debug
325
        )
326
        try:
327
            self.uvicorn_server = GlancesUvicornServer(config=uvicorn_config)
328
        except Exception as e:
329
            logger.critical(f'Error: Can not ran Glances Web server ({e})')
330
            self.uvicorn_server = None
331
        else:
332
            with self.uvicorn_server.run_in_thread():
333
                while not self.uvicorn_server.should_exit:
334
                    time.sleep(1)
335
336
    def end(self):
337
        """End the Web server"""
338
        if not self.args.disable_autodiscover and self.autodiscover_client:
339
            self.autodiscover_client.close()
340
        logger.info("Close the Web server")
341
342
    def _index(self, request: Request):
343
        """Return main index.html (/) file.
344
345
        Parameters are available through the request object.
346
        Example: http://localhost:61208/?refresh=5
347
348
        Note: This function is only called the first time the page is loaded.
349
        """
350
        refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time)))
351
352
        # Update the stat
353
        self.__update_stats()
354
355
        # Display
356
        return self._templates.TemplateResponse("index.html", {"request": request, "refresh_time": refresh_time})
357
358
    def _browser(self, request: Request):
359
        """Return main browser.html (/browser) file.
360
361
        Note: This function is only called the first time the page is loaded.
362
        """
363
        refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time)))
364
365
        # Display
366
        return self._templates.TemplateResponse("browser.html", {"request": request, "refresh_time": refresh_time})
367
368
    def _api_status(self):
369
        """Glances API RESTful implementation.
370
371
        Return a 200 status code.
372
        This entry point should be used to check the API health.
373
374
        See related issue:  Web server health check endpoint #1988
375
        """
376
377
        return GlancesJSONResponse({'version': __version__})
378
379
    def _events_clear_warning(self):
380
        """Glances API RESTful implementation.
381
382
        Return a 200 status code.
383
384
        It's a post message to clean warning events
385
        """
386
        glances_events.clean()
387
        return GlancesJSONResponse({})
388
389
    def _events_clear_all(self):
390
        """Glances API RESTful implementation.
391
392
        Return a 200 status code.
393
394
        It's a post message to clean all events
395
        """
396
        glances_events.clean(critical=True)
397
        return GlancesJSONResponse({})
398
399
    def _api_help(self):
400
        """Glances API RESTful implementation.
401
402
        Return the help data or 404 error.
403
        """
404
        try:
405
            plist = self.stats.get_plugin("help").get_view_data()
406
        except Exception as e:
407
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get help view data ({str(e)})")
408
409
        return GlancesJSONResponse(plist)
410
411
    def _api_plugins(self):
412
        """Glances API RESTFul implementation.
413
414
        @api {get} /api/%s/pluginslist Get plugins list
415
        @apiVersion 2.0
416
        @apiName pluginslist
417
        @apiGroup plugin
418
419
        @apiSuccess {String[]} Plugins list.
420
421
        @apiSuccessExample Success-Response:
422
            HTTP/1.1 200 OK
423
            [
424
               "load",
425
               "help",
426
               "ip",
427
               "memswap",
428
               "processlist",
429
               ...
430
            ]
431
432
         @apiError Cannot get plugin list.
433
434
         @apiErrorExample Error-Response:
435
            HTTP/1.1 404 Not Found
436
        """
437
        # Update the stat
438
        self.__update_stats()
439
440
        try:
441
            plist = self.plugins_list
442
        except Exception as e:
443
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin list ({str(e)})")
444
445
        return GlancesJSONResponse(plist)
446
447
    def _api_servers_list(self):
448
        """Glances API RESTful implementation.
449
450
        Return the JSON representation of the servers list (for browser mode)
451
        HTTP/200 if OK
452
        """
453
        # Update the servers list (and the stats for all the servers)
454
        self.__update_servers_list()
455
456
        return GlancesJSONResponse(self.servers_list.get_servers_list() if self.servers_list else [])
457
458
    @weak_lru_cache(maxsize=1, ttl=1)
459
    def _api_all(self):
460
        """Glances API RESTful implementation.
461
462
        Return the JSON representation of all the plugins
463
        HTTP/200 if OK
464
        HTTP/400 if plugin is not found
465
        HTTP/404 if others error
466
        """
467
        if self.args.debug:
468
            fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
469
            try:
470
                with builtins.open(fname) as f:
471
                    return f.read()
472
            except OSError:
473
                logger.debug(f"Debug file ({fname}) not found")
474
475
        # Update the stat
476
        self.__update_stats()
477
478
        try:
479
            # Get the RAW value of the stat ID
480
            statval = self.stats.getAllAsDict()
481
        except Exception as e:
482
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get stats ({str(e)})")
483
484
        return GlancesJSONResponse(statval)
485
486
    @weak_lru_cache(maxsize=1, ttl=1)
487
    def _api_all_limits(self):
488
        """Glances API RESTful implementation.
489
490
        Return the JSON representation of all the plugins limits
491
        HTTP/200 if OK
492
        HTTP/400 if plugin is not found
493
        HTTP/404 if others error
494
        """
495
        try:
496
            # Get the RAW value of the stat limits
497
            limits = self.stats.getAllLimitsAsDict()
498
        except Exception as e:
499
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get limits ({str(e)})")
500
501
        return GlancesJSONResponse(limits)
502
503
    @weak_lru_cache(maxsize=1, ttl=1)
504
    def _api_all_views(self):
505
        """Glances API RESTful implementation.
506
507
        Return the JSON representation of all the plugins views
508
        HTTP/200 if OK
509
        HTTP/400 if plugin is not found
510
        HTTP/404 if others error
511
        """
512
        try:
513
            # Get the RAW value of the stat view
514
            limits = self.stats.getAllViewsAsDict()
515
        except Exception as e:
516
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get views ({str(e)})")
517
518
        return GlancesJSONResponse(limits)
519
520
    @weak_lru_cache(maxsize=1, ttl=1)
521
    def _api(self, plugin: str):
522
        """Glances API RESTful implementation.
523
524
        Return the JSON representation of a given plugin
525
        HTTP/200 if OK
526
        HTTP/400 if plugin is not found
527
        HTTP/404 if others error
528
        """
529
        self._check_if_plugin_available(plugin)
530
531
        # Update the stat
532
        self.__update_stats()
533
534
        try:
535
            # Get the RAW value of the stat ID
536
            statval = self.stats.get_plugin(plugin).get_raw()
537
        except Exception as e:
538
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin {plugin} ({str(e)})")
539
540
        return GlancesJSONResponse(statval)
541
542
    def _check_if_plugin_available(self, plugin: str) -> None:
543
        if plugin in self.plugins_list:
544
            return
545
546
        raise HTTPException(
547
            status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
548
        )
549
550
    @weak_lru_cache(maxsize=1, ttl=1)
551
    def _api_top(self, plugin: str, nb: int = 0):
552
        """Glances API RESTful implementation.
553
554
        Return the JSON representation of a given plugin limited to the top nb items.
555
        It is used to reduce the payload of the HTTP response (example: processlist).
556
557
        HTTP/200 if OK
558
        HTTP/400 if plugin is not found
559
        HTTP/404 if others error
560
        """
561
        self._check_if_plugin_available(plugin)
562
563
        # Update the stat
564
        self.__update_stats()
565
566
        try:
567
            # Get the RAW value of the stat ID
568
            statval = self.stats.get_plugin(plugin).get_raw()
569
        except Exception as e:
570
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin {plugin} ({str(e)})")
571
572
        print(statval)
573
574
        if isinstance(statval, list):
575
            statval = statval[:nb]
576
577
        return GlancesJSONResponse(statval)
578
579
    @weak_lru_cache(maxsize=1, ttl=1)
580
    def _api_history(self, plugin: str, nb: int = 0):
581
        """Glances API RESTful implementation.
582
583
        Return the JSON representation of a given plugin history
584
        Limit to the last nb items (all if nb=0)
585
        HTTP/200 if OK
586
        HTTP/400 if plugin is not found
587
        HTTP/404 if others error
588
        """
589
        self._check_if_plugin_available(plugin)
590
591
        # Update the stat
592
        self.__update_stats()
593
594
        try:
595
            # Get the RAW value of the stat ID
596
            statval = self.stats.get_plugin(plugin).get_raw_history(nb=int(nb))
597
        except Exception as e:
598
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin history {plugin} ({str(e)})")
599
600
        return statval
601
602
    @weak_lru_cache(maxsize=1, ttl=1)
603
    def _api_limits(self, plugin: str):
604
        """Glances API RESTful implementation.
605
606
        Return the JSON limits of a given plugin
607
        HTTP/200 if OK
608
        HTTP/400 if plugin is not found
609
        HTTP/404 if others error
610
        """
611
        self._check_if_plugin_available(plugin)
612
613
        try:
614
            # Get the RAW value of the stat limits
615
            ret = self.stats.get_plugin(plugin).limits
616
        except Exception as e:
617
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get limits for plugin {plugin} ({str(e)})")
618
619
        return GlancesJSONResponse(ret)
620
621
    @weak_lru_cache(maxsize=1, ttl=1)
622
    def _api_views(self, plugin: str):
623
        """Glances API RESTful implementation.
624
625
        Return the JSON views of a given plugin
626
        HTTP/200 if OK
627
        HTTP/400 if plugin is not found
628
        HTTP/404 if others error
629
        """
630
        if plugin not in self.plugins_list:
631
            raise HTTPException(
632
                status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
633
            )
634
635
        try:
636
            # Get the RAW value of the stat views
637
            ret = self.stats.get_plugin(plugin).get_views()
638
        except Exception as e:
639
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get views for plugin {plugin} ({str(e)})")
640
641
        return GlancesJSONResponse(ret)
642
643
    def _api_item(self, plugin: str, item: str):
644
        """Glances API RESTful implementation.
645
646
        Return the JSON representation of the couple plugin/item
647
        HTTP/200 if OK
648
        HTTP/400 if plugin is not found
649
        HTTP/404 if others error
650
        """
651
        self._check_if_plugin_available(plugin)
652
653
        # Update the stat
654
        self.__update_stats()
655
656
        try:
657
            # Get the RAW value of the stat views
658
            ret = self.stats.get_plugin(plugin).get_raw_stats_item(item)
659
        except Exception as e:
660
            raise HTTPException(
661
                status.HTTP_404_NOT_FOUND,
662
                f"Cannot get item {item} in plugin {plugin} ({str(e)})",
663
            )
664
665
        return GlancesJSONResponse(ret)
666
667
    def _api_key(self, plugin: str, item: str, key: str):
668
        """Glances API RESTful implementation.
669
670
        Return the JSON representation of  plugin/item/key
671
        HTTP/200 if OK
672
        HTTP/400 if plugin is not found
673
        HTTP/404 if others error
674
        """
675
        self._check_if_plugin_available(plugin)
676
677
        # Update the stat
678
        self.__update_stats()
679
680
        try:
681
            # Get the RAW value of the stat views
682
            ret = self.stats.get_plugin(plugin).get_raw_stats_key(item, key)
683
        except Exception as e:
684
            raise HTTPException(
685
                status.HTTP_404_NOT_FOUND,
686
                f"Cannot get item {item} for key {key} in plugin {plugin} ({str(e)})",
687
            )
688
689
        return GlancesJSONResponse(ret)
690
691
    def _api_item_views(self, plugin: str, item: str):
692
        """Glances API RESTful implementation.
693
694
        Return the JSON view representation of the couple plugin/item
695
        HTTP/200 if OK
696
        HTTP/400 if plugin is not found
697
        HTTP/404 if others error
698
        """
699
        self._check_if_plugin_available(plugin)
700
701
        # Update the stat
702
        self.__update_stats()
703
704
        try:
705
            # Get the RAW value of the stat views
706
            ret = self.stats.get_plugin(plugin).get_views().get(item)
707
        except Exception as e:
708
            raise HTTPException(
709
                status.HTTP_404_NOT_FOUND,
710
                f"Cannot get item {item} in plugin view {plugin} ({str(e)})",
711
            )
712
713
        return GlancesJSONResponse(ret)
714
715
    def _api_key_views(self, plugin: str, item: str, key: str):
716
        """Glances API RESTful implementation.
717
718
        Return the JSON view representation of plugin/item/key
719
        HTTP/200 if OK
720
        HTTP/400 if plugin is not found
721
        HTTP/404 if others error
722
        """
723
        self._check_if_plugin_available(plugin)
724
725
        # Update the stat
726
        self.__update_stats()
727
728
        try:
729
            # Get the RAW value of the stat views
730
            ret = self.stats.get_plugin(plugin).get_views().get(key).get(item)
731
        except Exception as e:
732
            raise HTTPException(
733
                status.HTTP_404_NOT_FOUND,
734
                f"Cannot get item {item} for key {key} in plugin view {plugin} ({str(e)})",
735
            )
736
737
        return GlancesJSONResponse(ret)
738
739
    def _api_item_history(self, plugin: str, item: str, nb: int = 0):
740
        """Glances API RESTful implementation.
741
742
        Return the JSON representation of the couple plugin/history of item
743
        HTTP/200 if OK
744
        HTTP/400 if plugin is not found
745
        HTTP/404 if others error
746
747
        """
748
        self._check_if_plugin_available(plugin)
749
750
        # Update the stat
751
        self.__update_stats()
752
753
        try:
754
            # Get the RAW value of the stat history
755
            ret = self.stats.get_plugin(plugin).get_raw_history(item, nb=nb)
756
        except Exception as e:
757
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get history for plugin {plugin} ({str(e)})")
758
        else:
759
            return GlancesJSONResponse(ret)
760
761
    def _api_item_description(self, plugin: str, item: str):
762
        """Glances API RESTful implementation.
763
764
        Return the JSON representation of the couple plugin/item description
765
        HTTP/200 if OK
766
        HTTP/400 if plugin is not found
767
        HTTP/404 if others error
768
        """
769
        self._check_if_plugin_available(plugin)
770
771
        try:
772
            # Get the description
773
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'description')
774
        except Exception as e:
775
            raise HTTPException(
776
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} description for plugin {plugin} ({str(e)})"
777
            )
778
        else:
779
            return GlancesJSONResponse(ret)
780
781
    def _api_item_unit(self, plugin: str, item: str):
782
        """Glances API RESTful implementation.
783
784
        Return the JSON representation of the couple plugin/item unit
785
        HTTP/200 if OK
786
        HTTP/400 if plugin is not found
787
        HTTP/404 if others error
788
        """
789
        self._check_if_plugin_available(plugin)
790
791
        try:
792
            # Get the unit
793
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'unit')
794
        except Exception as e:
795
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get {item} unit for plugin {plugin} ({str(e)})")
796
        else:
797
            return GlancesJSONResponse(ret)
798
799
    def _api_value(self, plugin: str, item: str, value: Union[str, int, float]):
800
        """Glances API RESTful implementation.
801
802
        Return the process stats (dict) for the given item=value
803
        HTTP/200 if OK
804
        HTTP/400 if plugin is not found
805
        HTTP/404 if others error
806
        """
807
        self._check_if_plugin_available(plugin)
808
809
        # Update the stat
810
        self.__update_stats()
811
812
        try:
813
            # Get the RAW value
814
            ret = self.stats.get_plugin(plugin).get_raw_stats_value(item, value)
815
        except Exception as e:
816
            raise HTTPException(
817
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} = {value} for plugin {plugin} ({str(e)})"
818
            )
819
        else:
820
            return GlancesJSONResponse(ret)
821
822
    def _api_config(self):
823
        """Glances API RESTful implementation.
824
825
        Return the JSON representation of the Glances configuration file
826
        HTTP/200 if OK
827
        HTTP/404 if others error
828
        """
829
        try:
830
            # Get the RAW value of the config' dict
831
            args_json = self.config.as_dict()
832
        except Exception as e:
833
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config ({str(e)})")
834
        else:
835
            return GlancesJSONResponse(args_json)
836
837
    def _api_config_section(self, section: str):
838
        """Glances API RESTful implementation.
839
840
        Return the JSON representation of the Glances configuration section
841
        HTTP/200 if OK
842
        HTTP/400 if item is not found
843
        HTTP/404 if others error
844
        """
845
        config_dict = self.config.as_dict()
846
        if section not in config_dict:
847
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
848
849
        try:
850
            # Get the RAW value of the config' dict
851
            ret_section = config_dict[section]
852
        except Exception as e:
853
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config section {section} ({str(e)})")
854
855
        return GlancesJSONResponse(ret_section)
856
857
    def _api_config_section_item(self, section: str, item: str):
858
        """Glances API RESTful implementation.
859
860
        Return the JSON representation of the Glances configuration section/item
861
        HTTP/200 if OK
862
        HTTP/400 if item is not found
863
        HTTP/404 if others error
864
        """
865
        config_dict = self.config.as_dict()
866
        if section not in config_dict:
867
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
868
869
        try:
870
            # Get the RAW value of the config' dict section
871
            ret_section = config_dict[section]
872
        except Exception as e:
873
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config section {section} ({str(e)})")
874
875
        try:
876
            # Get the RAW value of the config' dict item
877
            ret_item = ret_section[item]
878
        except Exception as e:
879
            raise HTTPException(
880
                status.HTTP_404_NOT_FOUND, f"Cannot get item {item} in config section {section} ({str(e)})"
881
            )
882
883
        return GlancesJSONResponse(ret_item)
884
885
    def _api_args(self):
886
        """Glances API RESTful implementation.
887
888
        Return the JSON representation of the Glances command line arguments
889
        HTTP/200 if OK
890
        HTTP/404 if others error
891
        """
892
        try:
893
            # Get the RAW value of the args' dict
894
            # Use vars to convert namespace to dict
895
            # Source: https://docs.python.org/%s/library/functions.html#vars
896
            args_json = vars(self.args)
897
        except Exception as e:
898
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args ({str(e)})")
899
900
        return GlancesJSONResponse(args_json)
901
902
    def _api_args_item(self, item: str):
903
        """Glances API RESTful implementation.
904
905
        Return the JSON representation of the Glances command line arguments item
906
        HTTP/200 if OK
907
        HTTP/400 if item is not found
908
        HTTP/404 if others error
909
        """
910
        if item not in self.args:
911
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown argument item {item}")
912
913
        try:
914
            # Get the RAW value of the args' dict
915
            # Use vars to convert namespace to dict
916
            # Source: https://docs.python.org/%s/library/functions.html#vars
917
            args_json = vars(self.args)[item]
918
        except Exception as e:
919
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args item ({str(e)})")
920
921
        return GlancesJSONResponse(args_json)
922
923
    def _api_set_extended_processes(self, pid: str):
924
        """Glances API RESTful implementation.
925
926
        Set the extended process stats for the given PID
927
        HTTP/200 if OK
928
        HTTP/400 if PID is not found
929
        HTTP/404 if others error
930
        """
931
        process_stats = glances_processes.get_stats(int(pid))
932
933
        if not process_stats:
934
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
935
936
        glances_processes.extended_process = process_stats
937
938
        return GlancesJSONResponse(True)
939
940
    def _api_disable_extended_processes(self):
941
        """Glances API RESTful implementation.
942
943
        Disable extended process stats
944
        HTTP/200 if OK
945
        HTTP/400 if PID is not found
946
        HTTP/404 if others error
947
        """
948
        glances_processes.extended_process = None
949
950
        return GlancesJSONResponse(True)
951
952
    def _api_get_extended_processes(self):
953
        """Glances API RESTful implementation.
954
955
        Get the extended process stats (if set before)
956
        HTTP/200 if OK
957
        HTTP/400 if PID is not found
958
        HTTP/404 if others error
959
        """
960
        process_stats = glances_processes.get_extended_stats()
961
962
        if not process_stats:
963
            process_stats = {}
964
965
        return GlancesJSONResponse(process_stats)
966
967
    def _api_get_processes(self, pid: str):
968
        """Glances API RESTful implementation.
969
970
        Get the process stats for the given PID
971
        HTTP/200 if OK
972
        HTTP/400 if PID is not found
973
        HTTP/404 if others error
974
        """
975
        process_stats = glances_processes.get_stats(int(pid))
976
977
        if not process_stats:
978
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
979
980
        return GlancesJSONResponse(process_stats)
981