GlancesRestfulApi._api_args()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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