Test Failed
Push — develop ( df4b31...c89eff )
by Nicolas
03:00
created

GlancesRestfulApi._api_top()   A

Complexity

Conditions 3

Size

Total Lines 26
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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