GlancesRestfulApi._api_get_extended_processes()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nop 1
dl 0
loc 14
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'{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_raw()
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
            # TODO in #3211: use get_export instead but break API
590
            statval = self.stats.get_plugin(plugin).get_raw()
591
        except Exception as e:
592
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin {plugin} ({str(e)})")
593
594
        if isinstance(statval, list):
595
            statval = statval[:nb]
596
597
        return GlancesJSONResponse(statval)
598
599
    def _api_history(self, plugin: str, nb: int = 0):
600
        """Glances API RESTful implementation.
601
602
        Return the JSON representation of a given plugin history
603
        Limit to the last nb items (all if nb=0)
604
        HTTP/200 if OK
605
        HTTP/400 if plugin is not found
606
        HTTP/404 if others error
607
        """
608
        self._check_if_plugin_available(plugin)
609
610
        # Update the stat
611
        self.__update_stats(get_plugin_dependencies(plugin))
612
613
        try:
614
            # Get the RAW value of the stat ID
615
            statval = self.stats.get_plugin(plugin).get_raw_history(nb=int(nb))
616
        except Exception as e:
617
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get plugin history {plugin} ({str(e)})")
618
619
        return statval
620
621
    def _api_limits(self, plugin: str):
622
        """Glances API RESTful implementation.
623
624
        Return the JSON limits of a given plugin
625
        HTTP/200 if OK
626
        HTTP/400 if plugin is not found
627
        HTTP/404 if others error
628
        """
629
        self._check_if_plugin_available(plugin)
630
631
        try:
632
            # Get the RAW value of the stat limits
633
            ret = self.stats.get_plugin(plugin).get_limits()
634
        except Exception as e:
635
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get limits for plugin {plugin} ({str(e)})")
636
637
        return GlancesJSONResponse(ret)
638
639
    def _api_views(self, plugin: str):
640
        """Glances API RESTful implementation.
641
642
        Return the JSON views of a given plugin
643
        HTTP/200 if OK
644
        HTTP/400 if plugin is not found
645
        HTTP/404 if others error
646
        """
647
        if plugin not in self.plugins_list:
648
            raise HTTPException(
649
                status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
650
            )
651
652
        try:
653
            # Get the RAW value of the stat views
654
            ret = self.stats.get_plugin(plugin).get_views()
655
        except Exception as e:
656
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get views for plugin {plugin} ({str(e)})")
657
658
        return GlancesJSONResponse(ret)
659
660
    def _api_item(self, plugin: str, item: str):
661
        """Glances API RESTful implementation.
662
663
        Return the JSON representation of the couple plugin/item
664
        HTTP/200 if OK
665
        HTTP/400 if plugin is not found
666
        HTTP/404 if others error
667
        """
668
        self._check_if_plugin_available(plugin)
669
670
        # Update the stat
671
        self.__update_stats(get_plugin_dependencies(plugin))
672
673
        try:
674
            # Get the RAW value of the stat views
675
            # TODO in #3211: use a non existing (to be created) get_export_item instead but break API
676
            ret = self.stats.get_plugin(plugin).get_raw_stats_item(item)
677
        except Exception as e:
678
            raise HTTPException(
679
                status.HTTP_404_NOT_FOUND,
680
                f"Cannot get item {item} in plugin {plugin} ({str(e)})",
681
            )
682
683
        return GlancesJSONResponse(ret)
684
685
    def _api_key(self, plugin: str, item: str, key: str):
686
        """Glances API RESTful implementation.
687
688
        Return the JSON representation of  plugin/item/key
689
        HTTP/200 if OK
690
        HTTP/400 if plugin is not found
691
        HTTP/404 if others error
692
        """
693
        self._check_if_plugin_available(plugin)
694
695
        # Update the stat
696
        self.__update_stats(get_plugin_dependencies(plugin))
697
698
        try:
699
            # Get the RAW value of the stat views
700
            # TODO in #3211: use a non existing (to be created) get_export_key instead but break API
701
            ret = self.stats.get_plugin(plugin).get_raw_stats_key(item, key)
702
        except Exception as e:
703
            raise HTTPException(
704
                status.HTTP_404_NOT_FOUND,
705
                f"Cannot get item {item} for key {key} in plugin {plugin} ({str(e)})",
706
            )
707
708
        return GlancesJSONResponse(ret)
709
710
    def _api_item_views(self, plugin: str, item: str):
711
        """Glances API RESTful implementation.
712
713
        Return the JSON view representation of the couple plugin/item
714
        HTTP/200 if OK
715
        HTTP/400 if plugin is not found
716
        HTTP/404 if others error
717
        """
718
        self._check_if_plugin_available(plugin)
719
720
        # Update the stat
721
        self.__update_stats(get_plugin_dependencies(plugin))
722
723
        try:
724
            # Get the RAW value of the stat views
725
            ret = self.stats.get_plugin(plugin).get_views().get(item)
726
        except Exception as e:
727
            raise HTTPException(
728
                status.HTTP_404_NOT_FOUND,
729
                f"Cannot get item {item} in plugin view {plugin} ({str(e)})",
730
            )
731
732
        return GlancesJSONResponse(ret)
733
734
    def _api_key_views(self, plugin: str, item: str, key: str):
735
        """Glances API RESTful implementation.
736
737
        Return the JSON view representation of plugin/item/key
738
        HTTP/200 if OK
739
        HTTP/400 if plugin is not found
740
        HTTP/404 if others error
741
        """
742
        self._check_if_plugin_available(plugin)
743
744
        # Update the stat
745
        self.__update_stats(get_plugin_dependencies(plugin))
746
747
        try:
748
            # Get the RAW value of the stat views
749
            ret = self.stats.get_plugin(plugin).get_views().get(key).get(item)
750
        except Exception as e:
751
            raise HTTPException(
752
                status.HTTP_404_NOT_FOUND,
753
                f"Cannot get item {item} for key {key} in plugin view {plugin} ({str(e)})",
754
            )
755
756
        return GlancesJSONResponse(ret)
757
758
    def _api_item_history(self, plugin: str, item: str, nb: int = 0):
759
        """Glances API RESTful implementation.
760
761
        Return the JSON representation of the couple plugin/history of item
762
        HTTP/200 if OK
763
        HTTP/400 if plugin is not found
764
        HTTP/404 if others error
765
766
        """
767
        self._check_if_plugin_available(plugin)
768
769
        # Update the stat
770
        self.__update_stats(get_plugin_dependencies(plugin))
771
772
        try:
773
            # Get the RAW value of the stat history
774
            ret = self.stats.get_plugin(plugin).get_raw_history(item, nb=nb)
775
        except Exception as e:
776
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get history for plugin {plugin} ({str(e)})")
777
        else:
778
            return GlancesJSONResponse(ret)
779
780
    def _api_item_description(self, plugin: str, item: str):
781
        """Glances API RESTful implementation.
782
783
        Return the JSON representation of the couple plugin/item description
784
        HTTP/200 if OK
785
        HTTP/400 if plugin is not found
786
        HTTP/404 if others error
787
        """
788
        self._check_if_plugin_available(plugin)
789
790
        try:
791
            # Get the description
792
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'description')
793
        except Exception as e:
794
            raise HTTPException(
795
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} description for plugin {plugin} ({str(e)})"
796
            )
797
        else:
798
            return GlancesJSONResponse(ret)
799
800
    def _api_item_unit(self, plugin: str, item: str):
801
        """Glances API RESTful implementation.
802
803
        Return the JSON representation of the couple plugin/item unit
804
        HTTP/200 if OK
805
        HTTP/400 if plugin is not found
806
        HTTP/404 if others error
807
        """
808
        self._check_if_plugin_available(plugin)
809
810
        try:
811
            # Get the unit
812
            ret = self.stats.get_plugin(plugin).get_item_info(item, 'unit')
813
        except Exception as e:
814
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get {item} unit for plugin {plugin} ({str(e)})")
815
        else:
816
            return GlancesJSONResponse(ret)
817
818
    def _api_value(self, plugin: str, item: str, value: Union[str, int, float]):
819
        """Glances API RESTful implementation.
820
821
        Return the process stats (dict) for the given item=value
822
        HTTP/200 if OK
823
        HTTP/400 if plugin is not found
824
        HTTP/404 if others error
825
        """
826
        self._check_if_plugin_available(plugin)
827
828
        # Update the stat
829
        self.__update_stats(get_plugin_dependencies(plugin))
830
831
        try:
832
            # Get the RAW value
833
            # TODO in #3211: use a non existing (to be created) get_export_item_value instead but break API
834
            ret = self.stats.get_plugin(plugin).get_raw_stats_value(item, value)
835
        except Exception as e:
836
            raise HTTPException(
837
                status.HTTP_404_NOT_FOUND, f"Cannot get {item} = {value} for plugin {plugin} ({str(e)})"
838
            )
839
        else:
840
            return GlancesJSONResponse(ret)
841
842
    def _api_config(self):
843
        """Glances API RESTful implementation.
844
845
        Return the JSON representation of the Glances configuration file
846
        HTTP/200 if OK
847
        HTTP/404 if others error
848
        """
849
        try:
850
            # Get the RAW value of the config' dict
851
            args_json = self.config.as_dict()
852
        except Exception as e:
853
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config ({str(e)})")
854
        else:
855
            return GlancesJSONResponse(args_json)
856
857
    def _api_config_section(self, section: str):
858
        """Glances API RESTful implementation.
859
860
        Return the JSON representation of the Glances configuration section
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
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
        return GlancesJSONResponse(ret_section)
876
877
    def _api_config_section_item(self, section: str, item: str):
878
        """Glances API RESTful implementation.
879
880
        Return the JSON representation of the Glances configuration section/item
881
        HTTP/200 if OK
882
        HTTP/400 if item is not found
883
        HTTP/404 if others error
884
        """
885
        config_dict = self.config.as_dict()
886
        if section not in config_dict:
887
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown configuration item {section}")
888
889
        try:
890
            # Get the RAW value of the config' dict section
891
            ret_section = config_dict[section]
892
        except Exception as e:
893
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get config section {section} ({str(e)})")
894
895
        try:
896
            # Get the RAW value of the config' dict item
897
            ret_item = ret_section[item]
898
        except Exception as e:
899
            raise HTTPException(
900
                status.HTTP_404_NOT_FOUND, f"Cannot get item {item} in config section {section} ({str(e)})"
901
            )
902
903
        return GlancesJSONResponse(ret_item)
904
905
    def _api_args(self):
906
        """Glances API RESTful implementation.
907
908
        Return the JSON representation of the Glances command line arguments
909
        HTTP/200 if OK
910
        HTTP/404 if others error
911
        """
912
        try:
913
            # Get the RAW value of the args' dict
914
            # Use vars to convert namespace to dict
915
            # Source: https://docs.python.org/%s/library/functions.html#vars
916
            args_json = vars(self.args)
917
        except Exception as e:
918
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args ({str(e)})")
919
920
        return GlancesJSONResponse(args_json)
921
922
    def _api_args_item(self, item: str):
923
        """Glances API RESTful implementation.
924
925
        Return the JSON representation of the Glances command line arguments item
926
        HTTP/200 if OK
927
        HTTP/400 if item is not found
928
        HTTP/404 if others error
929
        """
930
        if item not in self.args:
931
            raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown argument item {item}")
932
933
        try:
934
            # Get the RAW value of the args' dict
935
            # Use vars to convert namespace to dict
936
            # Source: https://docs.python.org/%s/library/functions.html#vars
937
            args_json = vars(self.args)[item]
938
        except Exception as e:
939
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args item ({str(e)})")
940
941
        return GlancesJSONResponse(args_json)
942
943
    def _api_set_extended_processes(self, pid: str):
944
        """Glances API RESTful implementation.
945
946
        Set the extended process stats for the given PID
947
        HTTP/200 if OK
948
        HTTP/400 if PID is not found
949
        HTTP/404 if others error
950
        """
951
        process_stats = glances_processes.get_stats(int(pid))
952
953
        if not process_stats:
954
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
955
956
        glances_processes.extended_process = process_stats
957
958
        return GlancesJSONResponse(True)
959
960
    def _api_disable_extended_processes(self):
961
        """Glances API RESTful implementation.
962
963
        Disable extended process stats
964
        HTTP/200 if OK
965
        HTTP/400 if PID is not found
966
        HTTP/404 if others error
967
        """
968
        glances_processes.extended_process = None
969
970
        return GlancesJSONResponse(True)
971
972
    def _api_get_extended_processes(self):
973
        """Glances API RESTful implementation.
974
975
        Get the extended process stats (if set before)
976
        HTTP/200 if OK
977
        HTTP/400 if PID is not found
978
        HTTP/404 if others error
979
        """
980
        process_stats = glances_processes.get_extended_stats()
981
982
        if not process_stats:
983
            process_stats = {}
984
985
        return GlancesJSONResponse(process_stats)
986
987
    def _api_get_processes(self, pid: str):
988
        """Glances API RESTful implementation.
989
990
        Get the process stats for the given PID
991
        HTTP/200 if OK
992
        HTTP/400 if PID is not found
993
        HTTP/404 if others error
994
        """
995
        process_stats = glances_processes.get_stats(int(pid))
996
997
        if not process_stats:
998
            raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
999
1000
        return GlancesJSONResponse(process_stats)
1001