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
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'{self.protocol}://{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
            # SSL
185
            self.ssl_keyfile = config.get_value('outputs', 'ssl_keyfile', default=None)
186
            self.ssl_keyfile_password = config.get_value('outputs', 'ssl_keyfile_password', default=None)
187
            self.ssl_certfile = config.get_value('outputs', 'ssl_certfile', default=None)
188
            self.protocol = 'https' if self.is_ssl() else 'http'
189
            logger.debug(f"Protocol for Resful API and WebUI: {self.protocol}")
190
191
    def is_ssl(self):
192
        """Return true if the Glances server use SSL."""
193
        return self.ssl_keyfile is not None and self.ssl_certfile is not None
194
195
    def __update_stats(self, plugins_list_to_update=None):
196
        # Never update more than 1 time per cached_time
197
        # Also update if specific plugins are requested
198
        # In  this case, lru_cache will handle the stat's update frequency
199
        if self.timer.finished() or plugins_list_to_update:
200
            self.stats.update(plugins_list_to_update=plugins_list_to_update)
201
            self.timer = Timer(self.args.cached_time)
202
203
    def __update_servers_list(self):
204
        # Never update more than 1 time per cached_time
205
        if self.timer.finished() and self.servers_list is not None:
206
            self.servers_list.update_servers_stats()
207
            self.timer = Timer(self.args.cached_time)
208
209
    def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
210
        """Check if a username/password combination is valid."""
211
        if creds.username == self.args.username:
212
            # check_password
213
            if self._password.check_password(self.args.password, self._password.get_hash(creds.password)):
214
                return creds.username
215
216
        # If the username/password combination is invalid, return an HTTP 401
217
        raise HTTPException(
218
            status.HTTP_401_UNAUTHORIZED, "Incorrect username or password", {"WWW-Authenticate": "Basic"}
219
        )
220
221
    def _logo(self):
222
        return rf"""
223
  _____ _
224
 / ____| |
225
| |  __| | __ _ _ __   ___ ___  ___
226
| | |_ | |/ _` | '_ \ / __/ _ \/ __|
227
| |__| | | (_| | | | | (_|  __/\__
228
 \_____|_|\__,_|_| |_|\___\___||___/ {__version__}
229
        """
230
231
    def _router(self) -> APIRouter:
232
        """Define a custom router for Glances path."""
233
        base_path = f'/api/{self.API_VERSION}'
234
        plugin_path = f"{base_path}/{{plugin}}"
235
236
        # Create the main router
237
        router = APIRouter(prefix=self.url_prefix)
238
239
        # REST API route definition
240
        # ==========================
241
242
        # HEAD
243
        router.add_api_route(f'{base_path}/status', self._api_status, methods=['HEAD'])
244
245
        # POST
246
        router.add_api_route(f'{base_path}/events/clear/warning', self._events_clear_warning, methods=['POST'])
247
        router.add_api_route(f'{base_path}/events/clear/all', self._events_clear_all, methods=['POST'])
248
        router.add_api_route(
249
            f'{base_path}/processes/extended/disable', self._api_disable_extended_processes, methods=['POST']
250
        )
251
        router.add_api_route(
252
            f'{base_path}/processes/extended/{{pid}}', self._api_set_extended_processes, methods=['POST']
253
        )
254
255
        # GET
256
        router.add_api_route(f'{base_path}/status', self._api_status, methods=['GET'])
257
        route_mapping = {
258
            f'{base_path}/config': self._api_config,
259
            f'{base_path}/config/{{section}}': self._api_config_section,
260
            f'{base_path}/config/{{section}}/{{item}}': self._api_config_section_item,
261
            f'{base_path}/args': self._api_args,
262
            f'{base_path}/args/{{item}}': self._api_args_item,
263
            f'{base_path}/help': self._api_help,
264
            f'{base_path}/all': self._api_all,
265
            f'{base_path}/all/limits': self._api_all_limits,
266
            f'{base_path}/all/views': self._api_all_views,
267
            f'{base_path}/pluginslist': self._api_plugins,
268
            f'{base_path}/serverslist': self._api_servers_list,
269
            f'{base_path}/processes/extended': self._api_get_extended_processes,
270
            f'{base_path}/processes/{{pid}}': self._api_get_processes,
271
            f'{plugin_path}': self._api,
272
            f'{plugin_path}/history': self._api_history,
273
            f'{plugin_path}/history/{{nb}}': self._api_history,
274
            f'{plugin_path}/top/{{nb}}': self._api_top,
275
            f'{plugin_path}/limits': self._api_limits,
276
            f'{plugin_path}/views': self._api_views,
277
            f'{plugin_path}/{{item}}': self._api_item,
278
            f'{plugin_path}/{{item}}/views': self._api_item_views,
279
            f'{plugin_path}/{{item}}/history': self._api_item_history,
280
            f'{plugin_path}/{{item}}/history/{{nb}}': self._api_item_history,
281
            f'{plugin_path}/{{item}}/description': self._api_item_description,
282
            f'{plugin_path}/{{item}}/unit': self._api_item_unit,
283
            f'{plugin_path}/{{item}}/value/{{value:path}}': self._api_value,
284
            f'{plugin_path}/{{item}}/{{key}}': self._api_key,
285
            f'{plugin_path}/{{item}}/{{key}}/views': self._api_key_views,
286
        }
287
        for path, endpoint in route_mapping.items():
288
            router.add_api_route(path, endpoint)
289
290
        # Logo
291
        print(self._logo())
292
293
        # Browser WEBUI
294
        if hasattr(self.args, 'browser') and self.args.browser:
295
            # Template for the root browser.html file
296
            router.add_api_route('/browser', self._browser, response_class=HTMLResponse)
297
298
            # Statics files
299
            self._app.mount(self.url_prefix + '/static', StaticFiles(directory=self.STATIC_PATH), name="static")
300
            logger.debug(f"The Browser WebUI is enable and got statics files in {self.STATIC_PATH}")
301
302
            bindmsg = f'Glances Browser Web User Interface started on {self.bind_url}browser'
303
            logger.info(bindmsg)
304
            print(bindmsg)
305
306
        # WEBUI
307
        if not self.args.disable_webui:
308
            # Template for the root index.html file
309
            router.add_api_route('/', self._index, response_class=HTMLResponse)
310
311
            # Statics files
312
            self._app.mount(self.url_prefix + '/static', StaticFiles(directory=self.STATIC_PATH), name="static")
313
            logger.debug(f"The WebUI is enable and got statics files in {self.STATIC_PATH}")
314
315
            bindmsg = f'Glances Web User Interface started on {self.bind_url}'
316
            logger.info(bindmsg)
317
            print(bindmsg)
318
        else:
319
            logger.info('The WebUI is disable (--disable-webui)')
320
321
        # Restful API
322
        bindmsg = f'Glances RESTful API Server started on {self.bind_url}api/{self.API_VERSION}'
323
        logger.info(bindmsg)
324
        print(bindmsg)
325
326
        return router
327
328
    def start(self, stats: GlancesStats) -> None:
329
        """Start the bottle."""
330
        # Init stats
331
        self.stats = stats
332
333
        # Init plugin list
334
        self.plugins_list = self.stats.getPluginsList()
335
336
        if self.args.open_web_browser:
337
            # Implementation of the issue #946
338
            # Try to open the Glances Web UI in the default Web browser if:
339
            # 1) --open-web-browser option is used
340
            # 2) Glances standalone mode is running on Windows OS
341
            webbrowser.open(self.bind_url, new=2, autoraise=1)
342
343
        # Start Uvicorn server
344
        self._start_uvicorn()
345
346
    def _start_uvicorn(self):
347
        # Run the Uvicorn Web server
348
        uvicorn_config = uvicorn.Config(
349
            self._app,
350
            host=self.args.bind_address,
351
            port=self.args.port,
352
            access_log=self.args.debug,
353
            ssl_keyfile=self.ssl_keyfile,
354
            ssl_certfile=self.ssl_certfile,
355
        )
356
        try:
357
            self.uvicorn_server = GlancesUvicornServer(config=uvicorn_config)
358
        except Exception as e:
359
            logger.critical(f'Error: Can not ran Glances Web server ({e})')
360
            self.uvicorn_server = None
361
        else:
362
            with self.uvicorn_server.run_in_thread():
363
                while not self.uvicorn_server.should_exit:
364
                    time.sleep(1)
365
366
    def end(self):
367
        """End the Web server"""
368
        if not self.args.disable_autodiscover and self.autodiscover_client:
369
            self.autodiscover_client.close()
370
        logger.info("Close the Web server")
371
372
    def _index(self, request: Request):
373
        """Return main index.html (/) file.
374
375
        Parameters are available through the request object.
376
        Example: http://localhost:61208/?refresh=5
377
378
        Note: This function is only called the first time the page is loaded.
379
        """
380
        refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time)))
381
382
        # Update the stat
383
        self.__update_stats()
384
385
        # Display
386
        return self._templates.TemplateResponse("index.html", {"request": request, "refresh_time": refresh_time})
387
388
    def _browser(self, request: Request):
389
        """Return main browser.html (/browser) file.
390
391
        Note: This function is only called the first time the page is loaded.
392
        """
393
        refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time)))
394
395
        # Display
396
        return self._templates.TemplateResponse("browser.html", {"request": request, "refresh_time": refresh_time})
397
398
    def _api_status(self):
399
        """Glances API RESTful implementation.
400
401
        Return a 200 status code.
402
        This entry point should be used to check the API health.
403
404
        See related issue:  Web server health check endpoint #1988
405
        """
406
407
        return GlancesJSONResponse({'version': __version__})
408
409
    def _events_clear_warning(self):
410
        """Glances API RESTful implementation.
411
412
        Return a 200 status code.
413
414
        It's a post message to clean warning events
415
        """
416
        glances_events.clean()
417
        return GlancesJSONResponse({})
418
419
    def _events_clear_all(self):
420
        """Glances API RESTful implementation.
421
422
        Return a 200 status code.
423
424
        It's a post message to clean all events
425
        """
426
        glances_events.clean(critical=True)
427
        return GlancesJSONResponse({})
428
429
    def _api_help(self):
430
        """Glances API RESTful implementation.
431
432
        Return the help data or 404 error.
433
        """
434
        try:
435
            plist = self.stats.get_plugin("help").get_view_data()
436
        except Exception as e:
437
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get help view data ({str(e)})")
438
439
        return GlancesJSONResponse(plist)
440
441
    def _api_plugins(self):
442
        """Glances API RESTFul implementation.
443
444
        @api {get} /api/%s/pluginslist Get plugins list
445
        @apiVersion 2.0
446
        @apiName pluginslist
447
        @apiGroup plugin
448
449
        @apiSuccess {String[]} Plugins list.
450
451
        @apiSuccessExample Success-Response:
452
            HTTP/1.1 200 OK
453
            [
454
               "load",
455
               "help",
456
               "ip",
457
               "memswap",
458
               "processlist",
459
               ...
460
            ]
461
462
         @apiError Cannot get plugin list.
463
464
         @apiErrorExample Error-Response:
465
            HTTP/1.1 404 Not Found
466
        """
467
        # Update the stat
468
        # TODO: Why ??? Try to comment it
469
        # self.__update_stats()
470
471
        try:
472
            plist = self.plugins_list
473
        except Exception as e:
474
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin list ({str(e)})")
475
476
        return GlancesJSONResponse(plist)
477
478
    def _api_servers_list(self):
479
        """Glances API RESTful implementation.
480
481
        Return the JSON representation of the servers list (for browser mode)
482
        HTTP/200 if OK
483
        """
484
        # Update the servers list (and the stats for all the servers)
485
        self.__update_servers_list()
486
487
        return GlancesJSONResponse(self.servers_list.get_servers_list() if self.servers_list else [])
488
489
    # Comment this solve an issue on Home Assistant See #3238
490
    def _api_all(self):
491
        """Glances API RESTful implementation.
492
493
        Return the JSON representation of all the plugins
494
        HTTP/200 if OK
495
        HTTP/400 if plugin is not found
496
        HTTP/404 if others error
497
        """
498
499
        # Update the stat
500
        self.__update_stats()
501
502
        try:
503
            # Get the RAW value of the stat ID
504
            # TODO in #3211: use getAllExportsAsDict instead but break UI for uptime, processlist, others ?
505
            statval = self.stats.getAllAsDict()
506
        except Exception as e:
507
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get stats ({str(e)})")
508
509
        return GlancesJSONResponse(statval)
510
511
    def _api_all_limits(self):
512
        """Glances API RESTful implementation.
513
514
        Return the JSON representation of all the plugins limits
515
        HTTP/200 if OK
516
        HTTP/400 if plugin is not found
517
        HTTP/404 if others error
518
        """
519
        try:
520
            # Get the RAW value of the stat limits
521
            limits = self.stats.getAllLimitsAsDict()
522
        except Exception as e:
523
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get limits ({str(e)})")
524
525
        return GlancesJSONResponse(limits)
526
527
    def _api_all_views(self):
528
        """Glances API RESTful implementation.
529
530
        Return the JSON representation of all the plugins views
531
        HTTP/200 if OK
532
        HTTP/400 if plugin is not found
533
        HTTP/404 if others error
534
        """
535
        try:
536
            # Get the RAW value of the stat view
537
            limits = self.stats.getAllViewsAsDict()
538
        except Exception as e:
539
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get views ({str(e)})")
540
541
        return GlancesJSONResponse(limits)
542
543
    def _api(self, plugin: str):
544
        """Glances API RESTful implementation.
545
546
        Return the JSON representation of a given plugin
547
        HTTP/200 if OK
548
        HTTP/400 if plugin is not found
549
        HTTP/404 if others error
550
        """
551
        self._check_if_plugin_available(plugin)
552
553
        # Update the stat
554
        self.__update_stats(get_plugin_dependencies(plugin))
555
556
        try:
557
            # Get the RAW value of the stat ID
558
            statval = self.stats.get_plugin(plugin).get_api()
559
        except Exception as e:
560
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin {plugin} ({str(e)})")
561
562
        return GlancesJSONResponse(statval)
563
564
    def _check_if_plugin_available(self, plugin: str) -> None:
565
        if plugin in self.plugins_list:
566
            return
567
568
        raise HTTPException(
569
            status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
570
        )
571
572
    def _api_top(self, plugin: str, nb: int = 0):
573
        """Glances API RESTful implementation.
574
575
        Return the JSON representation of a given plugin limited to the top nb items.
576
        It is used to reduce the payload of the HTTP response (example: processlist).
577
578
        HTTP/200 if OK
579
        HTTP/400 if plugin is not found
580
        HTTP/404 if others error
581
        """
582
        self._check_if_plugin_available(plugin)
583
584
        # Update the stat
585
        self.__update_stats(get_plugin_dependencies(plugin))
586
587
        try:
588
            # Get the RAW value of the stat ID
589
            statval = self.stats.get_plugin(plugin).get_api()
590
        except Exception as e:
591
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin {plugin} ({str(e)})")
592
593
        if isinstance(statval, list):
594
            statval = statval[:nb]
595
596
        return GlancesJSONResponse(statval)
597
598
    def _api_history(self, plugin: str, nb: int = 0):
599
        """Glances API RESTful implementation.
600
601
        Return the JSON representation of a given plugin history
602
        Limit to the last nb items (all if nb=0)
603
        HTTP/200 if OK
604
        HTTP/400 if plugin is not found
605
        HTTP/404 if others error
606
        """
607
        self._check_if_plugin_available(plugin)
608
609
        # Update the stat
610
        self.__update_stats(get_plugin_dependencies(plugin))
611
612
        try:
613
            # Get the RAW value of the stat ID
614
            statval = self.stats.get_plugin(plugin).get_raw_history(nb=int(nb))
615
        except Exception as e:
616
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin history {plugin} ({str(e)})")
617
618
        return statval
619
620
    def _api_limits(self, plugin: str):
621
        """Glances API RESTful implementation.
622
623
        Return the JSON limits of a given plugin
624
        HTTP/200 if OK
625
        HTTP/400 if plugin is not found
626
        HTTP/404 if others error
627
        """
628
        self._check_if_plugin_available(plugin)
629
630
        try:
631
            # Get the RAW value of the stat limits
632
            ret = self.stats.get_plugin(plugin).get_limits()
633
        except Exception as e:
634
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get limits for plugin {plugin} ({str(e)})")
635
636
        return GlancesJSONResponse(ret)
637
638
    def _api_views(self, plugin: str):
639
        """Glances API RESTful implementation.
640
641
        Return the JSON views of a given plugin
642
        HTTP/200 if OK
643
        HTTP/400 if plugin is not found
644
        HTTP/404 if others error
645
        """
646
        if plugin not in self.plugins_list:
647
            raise HTTPException(
648
                status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
649
            )
650
651
        try:
652
            # Get the RAW value of the stat views
653
            ret = self.stats.get_plugin(plugin).get_views()
654
        except Exception as e:
655
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get views for plugin {plugin} ({str(e)})")
656
657
        return GlancesJSONResponse(ret)
658
659
    def _api_item(self, plugin: str, item: str):
660
        """Glances API RESTful implementation.
661
662
        Return the JSON representation of the couple plugin/item
663
        HTTP/200 if OK
664
        HTTP/400 if plugin is not found
665
        HTTP/404 if others error
666
        """
667
        self._check_if_plugin_available(plugin)
668
669
        # Update the stat
670
        self.__update_stats(get_plugin_dependencies(plugin))
671
672
        try:
673
            # Get the RAW value of the stat views
674
            # TODO in #3211: use a non existing (to be created) get_export_item instead but break API
675
            ret = self.stats.get_plugin(plugin).get_raw_stats_item(item)
676
        except Exception as e:
677
            raise HTTPException(
678
                status.HTTP_404_NOT_FOUND,
679
                f"Cannot get item {item} in plugin {plugin} ({str(e)})",
680
            )
681
682
        return GlancesJSONResponse(ret)
683
684
    def _api_key(self, plugin: str, item: str, key: str):
685
        """Glances API RESTful implementation.
686
687
        Return the JSON representation of  plugin/item/key
688
        HTTP/200 if OK
689
        HTTP/400 if plugin is not found
690
        HTTP/404 if others error
691
        """
692
        self._check_if_plugin_available(plugin)
693
694
        # Update the stat
695
        self.__update_stats(get_plugin_dependencies(plugin))
696
697
        try:
698
            # Get the RAW value of the stat views
699
            # TODO in #3211: use a non existing (to be created) get_export_key instead but break API
700
            ret = self.stats.get_plugin(plugin).get_raw_stats_key(item, key)
701
        except Exception as e:
702
            raise HTTPException(
703
                status.HTTP_404_NOT_FOUND,
704
                f"Cannot get item {item} for key {key} in plugin {plugin} ({str(e)})",
705
            )
706
707
        return GlancesJSONResponse(ret)
708
709
    def _api_item_views(self, plugin: str, item: str):
710
        """Glances API RESTful implementation.
711
712
        Return the JSON view representation of the couple plugin/item
713
        HTTP/200 if OK
714
        HTTP/400 if plugin is not found
715
        HTTP/404 if others error
716
        """
717
        self._check_if_plugin_available(plugin)
718
719
        # Update the stat
720
        self.__update_stats(get_plugin_dependencies(plugin))
721
722
        try:
723
            # Get the RAW value of the stat views
724
            ret = self.stats.get_plugin(plugin).get_views().get(item)
725
        except Exception as e:
726
            raise HTTPException(
727
                status.HTTP_404_NOT_FOUND,
728
                f"Cannot get item {item} in plugin view {plugin} ({str(e)})",
729
            )
730
731
        return GlancesJSONResponse(ret)
732
733
    def _api_key_views(self, plugin: str, item: str, key: str):
734
        """Glances API RESTful implementation.
735
736
        Return the JSON view representation of plugin/item/key
737
        HTTP/200 if OK
738
        HTTP/400 if plugin is not found
739
        HTTP/404 if others error
740
        """
741
        self._check_if_plugin_available(plugin)
742
743
        # Update the stat
744
        self.__update_stats(get_plugin_dependencies(plugin))
745
746
        try:
747
            # Get the RAW value of the stat views
748
            ret = self.stats.get_plugin(plugin).get_views().get(key).get(item)
749
        except Exception as e:
750
            raise HTTPException(
751
                status.HTTP_404_NOT_FOUND,
752
                f"Cannot get item {item} for key {key} in plugin view {plugin} ({str(e)})",
753
            )
754
755
        return GlancesJSONResponse(ret)
756
757
    def _api_item_history(self, plugin: str, item: str, nb: int = 0):
758
        """Glances API RESTful implementation.
759
760
        Return the JSON representation of the couple plugin/history of item
761
        HTTP/200 if OK
762
        HTTP/400 if plugin is not found
763
        HTTP/404 if others error
764
765
        """
766
        self._check_if_plugin_available(plugin)
767
768
        # Update the stat
769
        self.__update_stats(get_plugin_dependencies(plugin))
770
771
        try:
772
            # Get the RAW value of the stat history
773
            ret = self.stats.get_plugin(plugin).get_raw_history(item, nb=nb)
774
        except Exception as e:
775
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get history for plugin {plugin} ({str(e)})")
776
        else:
777
            return GlancesJSONResponse(ret)
778
779
    def _api_item_description(self, plugin: str, item: str):
780
        """Glances API RESTful implementation.
781
782
        Return the JSON representation of the couple plugin/item description
783
        HTTP/200 if OK
784
        HTTP/400 if plugin is not found
785
        HTTP/404 if others error
786
        """
787
        self._check_if_plugin_available(plugin)
788
789
        try:
790
            # Get the description
791
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'description')
792
        except Exception as e:
793
            raise HTTPException(
794
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} description for plugin {plugin} ({str(e)})"
795
            )
796
        else:
797
            return GlancesJSONResponse(ret)
798
799
    def _api_item_unit(self, plugin: str, item: str):
800
        """Glances API RESTful implementation.
801
802
        Return the JSON representation of the couple plugin/item unit
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
        try:
810
            # Get the unit
811
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'unit')
812
        except Exception as e:
813
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get {item} unit for plugin {plugin} ({str(e)})")
814
        else:
815
            return GlancesJSONResponse(ret)
816
817
    def _api_value(self, plugin: str, item: str, value: str | int | float):
818
        """Glances API RESTful implementation.
819
820
        Return the process stats (dict) for the given item=value
821
        HTTP/200 if OK
822
        HTTP/400 if plugin is not found
823
        HTTP/404 if others error
824
        """
825
        self._check_if_plugin_available(plugin)
826
827
        # Update the stat
828
        self.__update_stats(get_plugin_dependencies(plugin))
829
830
        try:
831
            # Get the RAW value
832
            # TODO in #3211: use a non existing (to be created) get_export_item_value instead but break API
833
            ret = self.stats.get_plugin(plugin).get_raw_stats_value(item, value)
834
        except Exception as e:
835
            raise HTTPException(
836
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} = {value} for plugin {plugin} ({str(e)})"
837
            )
838
        else:
839
            return GlancesJSONResponse(ret)
840
841
    def _api_config(self):
842
        """Glances API RESTful implementation.
843
844
        Return the JSON representation of the Glances configuration file
845
        HTTP/200 if OK
846
        HTTP/404 if others error
847
        """
848
        try:
849
            # Get the RAW value of the config' dict
850
            args_json = self.config.as_dict()
851
        except Exception as e:
852
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config ({str(e)})")
853
        else:
854
            return GlancesJSONResponse(args_json)
855
856
    def _api_config_section(self, section: str):
857
        """Glances API RESTful implementation.
858
859
        Return the JSON representation of the Glances configuration section
860
        HTTP/200 if OK
861
        HTTP/400 if item is not found
862
        HTTP/404 if others error
863
        """
864
        config_dict = self.config.as_dict()
865
        if section not in config_dict:
866
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
867
868
        try:
869
            # Get the RAW value of the config' dict
870
            ret_section = config_dict[section]
871
        except Exception as e:
872
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config section {section} ({str(e)})")
873
874
        return GlancesJSONResponse(ret_section)
875
876
    def _api_config_section_item(self, section: str, item: str):
877
        """Glances API RESTful implementation.
878
879
        Return the JSON representation of the Glances configuration section/item
880
        HTTP/200 if OK
881
        HTTP/400 if item is not found
882
        HTTP/404 if others error
883
        """
884
        config_dict = self.config.as_dict()
885
        if section not in config_dict:
886
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
887
888
        try:
889
            # Get the RAW value of the config' dict section
890
            ret_section = config_dict[section]
891
        except Exception as e:
892
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config section {section} ({str(e)})")
893
894
        try:
895
            # Get the RAW value of the config' dict item
896
            ret_item = ret_section[item]
897
        except Exception as e:
898
            raise HTTPException(
899
                status.HTTP_404_NOT_FOUND, f"Cannot get item {item} in config section {section} ({str(e)})"
900
            )
901
902
        return GlancesJSONResponse(ret_item)
903
904
    def _api_args(self):
905
        """Glances API RESTful implementation.
906
907
        Return the JSON representation of the Glances command line arguments
908
        HTTP/200 if OK
909
        HTTP/404 if others error
910
        """
911
        try:
912
            # Get the RAW value of the args' dict
913
            # Use vars to convert namespace to dict
914
            # Source: https://docs.python.org/%s/library/functions.html#vars
915
            args_json = vars(self.args)
916
        except Exception as e:
917
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args ({str(e)})")
918
919
        return GlancesJSONResponse(args_json)
920
921
    def _api_args_item(self, item: str):
922
        """Glances API RESTful implementation.
923
924
        Return the JSON representation of the Glances command line arguments item
925
        HTTP/200 if OK
926
        HTTP/400 if item is not found
927
        HTTP/404 if others error
928
        """
929
        if item not in self.args:
930
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown argument item {item}")
931
932
        try:
933
            # Get the RAW value of the args' dict
934
            # Use vars to convert namespace to dict
935
            # Source: https://docs.python.org/%s/library/functions.html#vars
936
            args_json = vars(self.args)[item]
937
        except Exception as e:
938
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args item ({str(e)})")
939
940
        return GlancesJSONResponse(args_json)
941
942
    def _api_set_extended_processes(self, pid: str):
943
        """Glances API RESTful implementation.
944
945
        Set the extended process stats for the given PID
946
        HTTP/200 if OK
947
        HTTP/400 if PID is not found
948
        HTTP/404 if others error
949
        """
950
        process_stats = glances_processes.get_stats(int(pid))
951
952
        if not process_stats:
953
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
954
955
        glances_processes.extended_process = process_stats
956
957
        return GlancesJSONResponse(True)
958
959
    def _api_disable_extended_processes(self):
960
        """Glances API RESTful implementation.
961
962
        Disable extended process stats
963
        HTTP/200 if OK
964
        HTTP/400 if PID is not found
965
        HTTP/404 if others error
966
        """
967
        glances_processes.extended_process = None
968
969
        return GlancesJSONResponse(True)
970
971
    def _api_get_extended_processes(self):
972
        """Glances API RESTful implementation.
973
974
        Get the extended process stats (if set before)
975
        HTTP/200 if OK
976
        HTTP/400 if PID is not found
977
        HTTP/404 if others error
978
        """
979
        process_stats = glances_processes.get_extended_stats()
980
981
        if not process_stats:
982
            process_stats = {}
983
984
        return GlancesJSONResponse(process_stats)
985
986
    def _api_get_processes(self, pid: str):
987
        """Glances API RESTful implementation.
988
989
        Get the process stats for the given PID
990
        HTTP/200 if OK
991
        HTTP/400 if PID is not found
992
        HTTP/404 if others error
993
        """
994
        process_stats = glances_processes.get_stats(int(pid))
995
996
        if not process_stats:
997
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
998
999
        return GlancesJSONResponse(process_stats)
1000
1001
1002
# End of GlancesRestfulApi class
1003