Implementation of a JWT token thanks to Jose lib

This commit is contained in:
nicolargo 2026-01-04 10:39:48 +01:00
parent 37d2fe13d0
commit 4181585e18
13 changed files with 972 additions and 548 deletions

View File

@ -37,13 +37,17 @@ click==8.1.8
colorama==0.4.6 ; sys_platform == 'win32'
# via click
cryptography==46.0.3
# via pysnmpcrypto
# via
# pysnmpcrypto
# python-jose
defusedxml==0.7.1
# via glances
dnspython==2.8.0
# via pymongo
docker==7.1.0
# via glances
ecdsa==0.19.1
# via python-jose
elastic-transport==9.2.1
# via elasticsearch
elasticsearch==9.2.1
@ -119,7 +123,10 @@ psycopg-binary==3.3.2 ; implementation_name != 'pypy'
pyarrow==22.0.0
# via influxdb3-python
pyasn1==0.6.1
# via pysnmp-lextudio
# via
# pysnmp-lextudio
# python-jose
# rsa
pycparser==2.23 ; (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') or (implementation_name == 'pypy' and platform_python_implementation == 'PyPy')
# via cffi
pydantic==2.12.5
@ -155,6 +162,8 @@ python-dateutil==2.9.0.post0
# influxdb
# influxdb-client
# influxdb3-python
python-jose==3.5.0
# via glances
pytz==2025.2
# via influxdb
pywin32==311 ; sys_platform == 'win32'
@ -174,6 +183,8 @@ requests==2.32.5
# influxdb
# podman
# pysmi-lextudio
rsa==4.9.1
# via python-jose
setuptools==80.9.0
# via
# influxdb-client
@ -182,6 +193,7 @@ shtab==1.8.0 ; sys_platform != 'win32'
# via glances
six==1.17.0
# via
# ecdsa
# glances
# influxdb
# python-dateutil

View File

@ -51,6 +51,7 @@ history_size=1200
# then configure this folder with the webui_root_path key
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
#
# CORS options
# Comma separated list of origins that should be permitted to make cross-origin requests.
# Default is *
@ -64,10 +65,18 @@ history_size=1200
# Comma separated list of HTTP request headers that should be supported for cross-origin requests.
# Default is *
#cors_headers=*
#
# Define SSL files (keyfile_password is optional)
#ssl_keyfile_password=kfp
#ssl_keyfile=./glances.local+3-key.pem
#ssl_certfile=./glances.local+3.pem
#
# JWT Authentication settings
# Secret key for signing JWT tokens (generate with: openssl rand -hex 32)
# If not set, a random key is generated per server instance (tokens won't survive restart)
#jwt_secret_key=your-secure-secret-key-here
# Token expiration time in minutes (default: 60)
#jwt_expire_minutes=60
##############################################################################
# Plugins

View File

@ -51,6 +51,7 @@ max_processes_display=25
# then configure this folder with the webui_root_path key
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
#
# CORS options
# Comma separated list of origins that should be permitted to make cross-origin requests.
# Default is *
@ -64,10 +65,18 @@ max_processes_display=25
# Comma separated list of HTTP request headers that should be supported for cross-origin requests.
# Default is *
#cors_headers=*
#
# Define SSL files (keyfile_password is optional)
#ssl_keyfile_password=kfp
#ssl_keyfile=./glances.local+3-key.pem
#ssl_certfile=./glances.local+3.pem
#
# JWT Authentication settings
# Secret key for signing JWT tokens (generate with: openssl rand -hex 32)
# If not set, a random key is generated per server instance (tokens won't survive restart)
#jwt_secret_key=your-secure-secret-key-here
# Token expiration time in minutes (default: 60)
#jwt_expire_minutes=60
##############################################################################
# Plugins

View File

@ -8,16 +8,22 @@ anyio==4.12.0
# via starlette
certifi==2025.11.12
# via requests
cffi==2.0.0 ; platform_python_implementation != 'PyPy'
# via cryptography
charset-normalizer==3.4.4
# via requests
click==8.1.8
# via uvicorn
colorama==0.4.6 ; sys_platform == 'win32'
# via click
cryptography==46.0.3
# via python-jose
defusedxml==0.7.1
# via glances
docker==7.1.0
# via glances
ecdsa==0.19.1
# via python-jose
exceptiongroup==1.2.2 ; python_full_version < '3.11'
# via anyio
fastapi==0.128.0
@ -38,12 +44,20 @@ podman==5.6.0
# via glances
psutil==7.2.1
# via glances
pyasn1==0.6.1
# via
# python-jose
# rsa
pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'
# via cffi
pydantic==2.12.5
# via fastapi
pydantic-core==2.41.5
# via pydantic
python-dateutil==2.9.0.post0
# via glances
python-jose==3.5.0
# via glances
pywin32==311 ; sys_platform == 'win32'
# via docker
requests==2.32.5
@ -51,10 +65,13 @@ requests==2.32.5
# docker
# glances
# podman
rsa==4.9.1
# via python-jose
shtab==1.8.0 ; sys_platform != 'win32'
# via glances
six==1.17.0
# via
# ecdsa
# glances
# python-dateutil
starlette==0.50.0
@ -64,6 +81,7 @@ tomli==2.0.2 ; python_full_version < '3.11'
typing-extensions==4.15.0
# via
# anyio
# cryptography
# fastapi
# pydantic
# pydantic-core

View File

@ -22,25 +22,25 @@ use the following code:
>>> gl = api.GlancesAPI()
>>> gl.cpu
{'cpucore': 16,
'ctx_switches': 472872341,
'ctx_switches': 541113367,
'guest': 0.0,
'idle': 93.5,
'interrupts': 410386146,
'iowait': 0.3,
'idle': 93.1,
'interrupts': 464358062,
'iowait': 0.5,
'irq': 0.0,
'nice': 0.0,
'soft_interrupts': 174928185,
'soft_interrupts': 197631605,
'steal': 0.0,
'syscalls': 0,
'system': 4.4,
'total': 7.2,
'user': 1.7}
'system': 4.6,
'total': 7.9,
'user': 1.9}
>>> gl.cpu.get("total")
7.2
7.9
>>> gl.mem.get("used")
12810799128
10525812760
>>> gl.auto_unit(gl.mem.get("used"))
11.9G
9.80G
If the stats return a list of items (like network interfaces or processes), you can
access them by their name:
@ -51,19 +51,19 @@ access them by their name:
['wlp0s20f3']
>>> gl.network["wlp0s20f3"]
{'alias': None,
'bytes_all': 214,
'bytes_all_gauge': 15142743463,
'bytes_all_rate_per_sec': 606.0,
'bytes_recv': 128,
'bytes_recv_gauge': 13975900109,
'bytes_recv_rate_per_sec': 362.0,
'bytes_sent': 86,
'bytes_sent_gauge': 1166843354,
'bytes_sent_rate_per_sec': 243.0,
'bytes_all': 1903,
'bytes_all_gauge': 15435196525,
'bytes_all_rate_per_sec': 13613.0,
'bytes_recv': 1731,
'bytes_recv_gauge': 14118055339,
'bytes_recv_rate_per_sec': 12382.0,
'bytes_sent': 172,
'bytes_sent_gauge': 1317141186,
'bytes_sent_rate_per_sec': 1230.0,
'interface_name': 'wlp0s20f3',
'key': 'interface_name',
'speed': 0,
'time_since_update': 0.3526580333709717}
'time_since_update': 0.13979172706604004}
Init Glances Python API
-----------------------
@ -95,32 +95,7 @@ Alert stats:
>>> type(gl.alert)
<class 'glances.plugins.alert.AlertPlugin'>
>>> gl.alert
[{'avg': 99.96957776029373,
'begin': 1767449522,
'count': 2,
'desc': '',
'end': -1,
'global_msg': 'High swap (paging) usage',
'max': 99.96957776029373,
'min': 99.96957776029373,
'sort': 'memory_percent',
'state': 'CRITICAL',
'sum': 199.93915552058746,
'top': ['code', 'code', 'code'],
'type': 'MEMSWAP'},
{'avg': 77.95894443790266,
'begin': 1767449522,
'count': 2,
'desc': '',
'end': -1,
'global_msg': 'High swap (paging) usage',
'max': 78.00605779103407,
'min': 77.91183108477124,
'sort': 'memory_percent',
'state': 'WARNING',
'sum': 155.91788887580532,
'top': [],
'type': 'MEM'}]
[]
Alert fields description:
@ -161,7 +136,7 @@ Ports stats:
'port': 0,
'refresh': 30,
'rtt_warning': None,
'status': 0.008168,
'status': 0.006093,
'timeout': 3}]
Ports fields description:
@ -202,14 +177,14 @@ Diskio stats:
>>> gl.diskio.get("nvme0n1")
{'disk_name': 'nvme0n1',
'key': 'disk_name',
'read_bytes': 29268151808,
'read_count': 1162807,
'read_bytes': 47595146752,
'read_count': 1611927,
'read_latency': 0,
'read_time': 313717,
'write_bytes': 44187964416,
'write_count': 3047678,
'read_time': 492221,
'write_bytes': 47440806912,
'write_count': 3244300,
'write_latency': 0,
'write_time': 2442776}
'write_time': 2733363}
Diskio fields description:
@ -294,11 +269,11 @@ Processcount stats:
>>> type(gl.processcount)
<class 'glances.plugins.processcount.ProcesscountPlugin'>
>>> gl.processcount
{'pid_max': 0, 'running': 1, 'sleeping': 442, 'thread': 2444, 'total': 597}
{'pid_max': 0, 'running': 1, 'sleeping': 412, 'thread': 1984, 'total': 561}
>>> gl.processcount.keys()
['total', 'running', 'sleeping', 'thread', 'pid_max']
>>> gl.processcount.get("total")
597
561
Processcount fields description:
@ -371,7 +346,7 @@ Percpu stats:
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 44.0,
'idle': 29.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -380,8 +355,8 @@ Percpu stats:
'softirq': 0.0,
'steal': 0.0,
'system': 10.0,
'total': 56.0,
'user': 0.0}
'total': 71.0,
'user': 1.0}
Percpu fields description:
@ -471,18 +446,18 @@ Network stats:
>>> gl.network.get("wlp0s20f3")
{'alias': None,
'bytes_all': 0,
'bytes_all_gauge': 15142743463,
'bytes_all_gauge': 15435196525,
'bytes_all_rate_per_sec': 0.0,
'bytes_recv': 0,
'bytes_recv_gauge': 13975900109,
'bytes_recv_gauge': 14118055339,
'bytes_recv_rate_per_sec': 0.0,
'bytes_sent': 0,
'bytes_sent_gauge': 1166843354,
'bytes_sent_gauge': 1317141186,
'bytes_sent_rate_per_sec': 0.0,
'interface_name': 'wlp0s20f3',
'key': 'interface_name',
'speed': 0,
'time_since_update': 0.00287628173828125}
'time_since_update': 0.00189208984375}
Network fields description:
@ -523,23 +498,23 @@ Cpu stats:
<class 'glances.plugins.cpu.CpuPlugin'>
>>> gl.cpu
{'cpucore': 16,
'ctx_switches': 472872341,
'ctx_switches': 541113367,
'guest': 0.0,
'idle': 93.5,
'interrupts': 410386146,
'iowait': 0.3,
'idle': 93.1,
'interrupts': 464358062,
'iowait': 0.5,
'irq': 0.0,
'nice': 0.0,
'soft_interrupts': 174928185,
'soft_interrupts': 197631605,
'steal': 0.0,
'syscalls': 0,
'system': 4.4,
'total': 7.2,
'user': 1.7}
'system': 4.6,
'total': 7.9,
'user': 1.9}
>>> gl.cpu.keys()
['total', 'user', 'nice', 'system', 'idle', 'iowait', 'irq', 'steal', 'guest', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls', 'cpucore']
>>> gl.cpu.get("total")
7.2
7.9
Cpu fields description:
@ -611,7 +586,7 @@ Amps stats:
'refresh': 3.0,
'regex': True,
'result': None,
'timer': 0.31592440605163574}
'timer': 0.16622281074523926}
Amps fields description:
@ -642,32 +617,32 @@ Processlist stats:
>>> gl.processlist
Return a dict of dict with key=<pid>
>>> gl.processlist.keys()
[1, 2, 3, 4, 5, 6, 7, 8, 10, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 65, 66, 67, 68, 69, 71, 72, 73, 74, 75, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 89, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 102, 103, 104, 105, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 118, 121, 122, 123, 124, 125, 126, 127, 128, 133, 135, 136, 137, 138, 139, 140, 142, 143, 144, 145, 146, 147, 148, 150, 154, 156, 157, 158, 166, 179, 188, 189, 218, 219, 239, 240, 258, 267, 268, 269, 270, 271, 273, 279, 280, 364, 367, 369, 370, 371, 372, 373, 450, 452, 613, 618, 619, 620, 627, 659, 660, 726, 757, 758, 787, 795, 970, 971, 986, 1037, 1040, 1042, 1043, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1056, 1058, 1059, 1064, 1065, 1218, 1219, 1223, 1275, 1277, 1278, 1279, 1320, 1327, 1534, 1537, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016, 2017, 2018, 2019, 2020, 2021, 2023, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2078, 2079, 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2117, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2133, 2134, 2135, 2136, 2137, 2138, 2139, 2140, 2141, 2142, 2143, 2144, 2145, 2149, 2603, 2604, 2605, 2606, 2613, 2615, 2744, 2745, 2746, 2750, 2751, 2755, 2762, 2774, 2781, 2788, 2791, 2796, 2799, 2802, 2813, 2820, 2823, 2887, 2904, 2905, 2916, 3031, 3038, 3081, 3181, 3182, 3186, 3191, 3192, 3240, 3382, 3383, 3670, 3671, 3695, 3698, 3807, 3817, 3818, 3822, 3827, 3828, 3846, 3853, 3874, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3982, 4194, 4596, 5146, 5166, 5172, 5179, 5189, 5190, 5193, 5195, 5197, 5207, 5263, 5267, 5274, 5318, 5333, 5421, 5425, 5441, 5453, 5457, 5492, 5505, 5509, 5593, 5612, 5620, 5621, 5634, 5657, 5658, 5661, 5664, 5665, 5667, 5669, 5672, 5675, 5678, 5680, 5685, 5687, 5693, 5695, 5700, 5708, 5726, 5734, 5829, 5832, 5833, 5844, 5861, 5871, 5914, 5921, 5931, 5941, 5954, 5957, 6017, 6063, 6087, 6093, 6094, 6123, 6154, 6206, 6244, 6475, 6505, 6552, 6559, 6562, 7227, 8412, 8526, 9631, 9649, 9661, 9731, 9734, 11010, 12121, 12123, 12124, 12138, 12192, 12239, 12278, 12321, 12335, 12336, 12417, 12686, 12726, 12735, 12929, 12931, 12933, 12934, 13048, 13115, 13536, 13665, 36430, 40117, 53755, 53764, 62488, 62637, 72064, 72065, 72120, 72139, 102070, 340569, 388396, 413445, 444052, 444061, 471623, 471714, 471892, 471899, 471919, 471929, 471954, 472557, 472566, 472570, 472785, 472854, 472861, 473314, 481055, 513901, 514243, 524431, 524432, 524447, 524457, 524551, 546233, 546254, 547205, 547220, 547714, 547732, 548091, 548102, 548622, 561234, 564559, 567573, 654061, 661802, 671732, 683980, 686191, 695926, 701770, 705018, 705167, 707900, 708398, 709364, 709585, 710008, 710952, 714751, 714947, 721023, 721204, 726507, 728005, 728006, 728008, 728465, 730137, 731844, 732902, 733166, 733167, 733841, 733865, 734183, 735109, 735551, 736180, 736316, 736396, 736607, 737036, 737183, 737369, 737866, 738311, 738732, 739425, 739889, 740008, 740325, 740438, 741044, 741346, 741856, 742102, 742589, 742967, 743001, 743292, 743314, 743466, 743467, 743468, 743469, 743470, 743471, 743554, 743752, 743753, 743869, 743926, 743942, 744023, 744188, 744189, 744276, 744281, 745624, 745766, 745875, 745878, 745879, 745882]
[1, 2, 3, 4, 5, 6, 7, 8, 10, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 65, 66, 67, 68, 69, 71, 72, 73, 74, 75, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 89, 90, 91, 92, 93, 95, 96, 97, 98, 99, 101, 102, 103, 104, 105, 107, 108, 109, 110, 111, 113, 114, 115, 116, 117, 118, 121, 122, 123, 124, 125, 126, 127, 128, 133, 135, 136, 137, 138, 139, 140, 142, 143, 144, 145, 146, 147, 148, 150, 154, 156, 157, 158, 166, 179, 188, 189, 218, 219, 239, 240, 258, 267, 268, 269, 270, 271, 273, 279, 280, 364, 367, 369, 370, 371, 372, 373, 450, 452, 613, 618, 619, 620, 627, 659, 660, 726, 757, 758, 787, 795, 970, 971, 986, 1037, 1040, 1042, 1043, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1056, 1058, 1059, 1064, 1065, 1218, 1219, 1223, 1275, 1277, 1278, 1279, 1320, 1327, 1534, 1537, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016, 2017, 2018, 2019, 2020, 2021, 2023, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2078, 2079, 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2117, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2133, 2134, 2135, 2136, 2137, 2138, 2139, 2140, 2141, 2142, 2143, 2144, 2145, 2149, 2603, 2604, 2605, 2606, 2613, 2615, 2744, 2745, 2746, 2750, 2751, 2755, 2762, 2774, 2781, 2788, 2791, 2796, 2799, 2802, 2813, 2820, 2823, 2887, 2904, 2905, 2916, 3031, 3038, 3081, 3181, 3182, 3186, 3191, 3192, 3240, 3382, 3383, 3670, 3671, 3695, 3698, 3807, 3817, 3818, 3822, 3827, 3828, 3846, 3853, 3874, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3982, 4194, 4596, 5166, 6562, 36430, 444052, 444061, 481055, 686191, 791450, 797892, 800110, 800150, 800151, 800285, 806441, 808018, 811099, 813104, 813640, 813665, 813669, 813681, 813682, 813683, 813685, 813687, 813689, 813699, 813749, 813753, 813760, 813779, 813795, 813873, 813876, 813892, 813903, 813906, 813949, 813962, 813963, 814046, 814083, 814091, 814092, 814106, 814107, 814128, 814129, 814130, 814133, 814135, 814138, 814140, 814143, 814147, 814150, 814152, 814156, 814158, 814162, 814170, 814177, 814191, 814192, 814284, 814293, 814295, 814306, 814324, 814353, 814356, 814384, 814388, 814397, 814410, 814412, 814429, 814438, 814507, 814547, 814577, 814588, 814592, 814614, 814657, 814874, 814875, 814918, 814987, 814992, 815007, 815154, 815196, 815236, 815252, 815324, 815555, 815952, 815959, 815967, 816178, 816198, 816205, 816444, 816560, 816572, 817391, 817393, 817394, 817439, 817491, 817540, 817578, 817620, 817648, 817649, 817933, 817952, 818000, 818044, 818206, 818210, 818211, 818213, 818223, 818670, 818773, 818940, 818964, 819036, 819054, 819077, 819153, 819443, 820148, 820306, 820835, 823577, 825801, 828210, 828339, 828612, 829135, 829999, 831005, 831221, 834846, 836649, 836885, 837028, 837106, 837195, 838107, 838946, 839073, 839074, 839230, 839243, 842229, 842773, 843197, 843881, 844002, 844988, 845334, 845619, 846865, 847391, 847596, 847599, 847816, 848168, 848169, 848296, 848442, 848637, 848983, 848984, 848985, 848986, 848987, 849482, 849620, 850436, 850804, 851714, 851777, 852971, 853655, 854520, 854521, 854586, 854783, 854845, 854876, 855229, 855232, 855233, 855236]
>>> gl.processlist.get("1")
{'cmdline': ['/sbin/init', 'splash'],
'cpu_percent': 0.0,
'cpu_times': {'children_system': 292.72,
'children_user': 5149.74,
'cpu_times': {'children_system': 11313.22,
'children_user': 46981.78,
'iowait': 0.0,
'system': 9.16,
'user': 13.08},
'system': 10.51,
'user': 14.18},
'gids': {'effective': 0, 'real': 0, 'saved': 0},
'io_counters': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'key': 'pid',
'memory_info': {'data': 7282688,
'dirty': 0,
'lib': 0,
'rss': 14585856,
'shared': 8134656,
'rss': 12140544,
'shared': 5771264,
'text': 45056,
'vms': 26632192},
'memory_percent': 0.0888145317633538,
'memory_percent': 0.07392481666570644,
'name': 'systemd',
'nice': 0,
'num_threads': 1,
'pid': 1,
'status': 'S',
'time_since_update': 0.6452903747558594,
'time_since_update': 0.40526819229125977,
'username': 'root'}
Processlist fields description:
@ -752,13 +727,13 @@ Load stats:
<class 'glances.plugins.load.LoadPlugin'>
>>> gl.load
{'cpucore': 16,
'min1': 0.869140625,
'min15': 1.04248046875,
'min5': 1.04052734375}
'min1': 0.62451171875,
'min15': 0.71826171875,
'min5': 0.67919921875}
>>> gl.load.keys()
['min1', 'min5', 'min15', 'cpucore']
>>> gl.load.get("min1")
0.869140625
0.62451171875
Load fields description:
@ -797,7 +772,7 @@ Sensors stats:
'label': 'Ambient',
'type': 'temperature_core',
'unit': 'C',
'value': 34,
'value': 36,
'warning': 0}
Sensors fields description:
@ -835,7 +810,7 @@ Uptime stats:
>>> type(gl.uptime)
<class 'glances.plugins.uptime.UptimePlugin'>
>>> gl.uptime
'6 days, 22:46:17'
'7 days, 18:13:52'
Uptime limits:
@ -854,11 +829,11 @@ Now stats:
>>> type(gl.now)
<class 'glances.plugins.now.NowPlugin'>
>>> gl.now
{'custom': '2026-01-03 15:12:02 CET', 'iso': '2026-01-03T15:12:02+01:00'}
{'custom': '2026-01-04 10:39:37 CET', 'iso': '2026-01-04T10:39:37+01:00'}
>>> gl.now.keys()
['iso', 'custom']
>>> gl.now.get("iso")
'2026-01-03T15:12:02+01:00'
'2026-01-04T10:39:37+01:00'
Now fields description:
@ -887,14 +862,14 @@ Fs stats:
['/', '/zsfpool']
>>> gl.fs.get("/")
{'device_name': '/dev/mapper/ubuntu--vg-ubuntu--lv',
'free': 582918623232,
'free': 582995017728,
'fs_type': 'ext4',
'key': 'mnt_point',
'mnt_point': '/',
'options': 'rw,relatime',
'percent': 38.8,
'size': 1003736440832,
'used': 369755312128}
'used': 369678917632}
Fs fields description:
@ -934,8 +909,8 @@ Wifi stats:
['wlp0s20f3']
>>> gl.wifi.get("wlp0s20f3")
{'key': 'ssid',
'quality_level': -60.0,
'quality_link': 50.0,
'quality_level': -62.0,
'quality_link': 48.0,
'ssid': 'wlp0s20f3'}
Wifi limits:
@ -959,13 +934,9 @@ Ip stats:
>>> type(gl.ip)
<class 'glances.plugins.ip.IpPlugin'>
>>> gl.ip
{'address': '192.168.1.26',
'mask': '255.255.255.0',
'mask_cidr': 24,
'public_address': '',
'public_info_human': ''}
{'address': '192.168.1.26', 'mask': '255.255.255.0', 'mask_cidr': 24}
>>> gl.ip.keys()
['address', 'mask', 'mask_cidr', 'public_address', 'public_info_human']
['address', 'mask', 'mask_cidr']
>>> gl.ip.get("address")
'192.168.1.26'
@ -1067,16 +1038,16 @@ Mem stats:
>>> type(gl.mem)
<class 'glances.plugins.mem.MemPlugin'>
>>> gl.mem
{'active': 5529219072,
'available': 3612026856,
'buffers': 130482176,
'cached': 3568499560,
'free': 1333854208,
'inactive': 7177965568,
'percent': 78.0,
'shared': 840089600,
{'active': 9791209472,
'available': 5897013224,
'buffers': 257441792,
'cached': 5707590504,
'free': 704032768,
'inactive': 4103938048,
'percent': 64.1,
'shared': 734093312,
'total': 16422825984,
'used': 12810799128}
'used': 10525812760}
>>> gl.mem.keys()
['total', 'available', 'percent', 'used', 'free', 'active', 'inactive', 'buffers', 'cached', 'shared']
>>> gl.mem.get("total")
@ -1146,19 +1117,19 @@ Quicklook stats:
>>> type(gl.quicklook)
<class 'glances.plugins.quicklook.QuicklookPlugin'>
>>> gl.quicklook
{'cpu': 7.2,
{'cpu': 7.9,
'cpu_hz': 4475000000.0,
'cpu_hz_current': 608909250.0,
'cpu_hz_current': 1034989000.0,
'cpu_log_core': 16,
'cpu_name': '13th Gen Intel(R) Core(TM) i7-13620H',
'cpu_phys_core': 10,
'load': 6.5,
'mem': 78.0,
'load': 4.5,
'mem': 64.1,
'percpu': [{'cpu_number': 0,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 44.0,
'idle': 29.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1167,13 +1138,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 10.0,
'total': 56.0,
'user': 0.0},
'total': 71.0,
'user': 1.0},
{'cpu_number': 1,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 39.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1182,13 +1153,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 45.0,
'user': 0.0},
'total': 61.0,
'user': 1.0},
{'cpu_number': 2,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 37.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1197,13 +1168,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 45.0,
'user': 1.0},
'total': 63.0,
'user': 2.0},
{'cpu_number': 3,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 41.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1211,59 +1182,14 @@ Quicklook stats:
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 45.0,
'system': 1.0,
'total': 59.0,
'user': 0.0},
{'cpu_number': 4,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 51.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 3.0,
'total': 49.0,
'user': 1.0},
{'cpu_number': 5,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 45.0,
'user': 0.0},
{'cpu_number': 6,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 45.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 8.0,
'total': 55.0,
'user': 3.0},
{'cpu_number': 7,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 28.0,
'idle': 27.0,
'interrupt': None,
'iowait': 1.0,
'irq': 0.0,
@ -1271,29 +1197,74 @@ Quicklook stats:
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 15.0,
'total': 72.0,
'user': 11.0},
{'cpu_number': 8,
'system': 3.0,
'total': 73.0,
'user': 7.0},
{'cpu_number': 5,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 52.0,
'idle': 41.0,
'interrupt': None,
'iowait': 2.0,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 48.0,
'system': 0.0,
'total': 59.0,
'user': 0.0},
{'cpu_number': 6,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 34.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 4.0,
'total': 66.0,
'user': 1.0},
{'cpu_number': 7,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 28.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 6.0,
'total': 72.0,
'user': 5.0},
{'cpu_number': 8,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 37.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
'key': 'cpu_number',
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 2.0,
'total': 63.0,
'user': 1.0},
{'cpu_number': 9,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 40.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1302,13 +1273,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 45.0,
'total': 60.0,
'user': 0.0},
{'cpu_number': 10,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 54.0,
'idle': 37.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1316,14 +1287,14 @@ Quicklook stats:
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 46.0,
'system': 2.0,
'total': 63.0,
'user': 1.0},
{'cpu_number': 11,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 56.0,
'idle': 41.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1332,13 +1303,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 44.0,
'total': 59.0,
'user': 0.0},
{'cpu_number': 12,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 39.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1346,14 +1317,14 @@ Quicklook stats:
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 45.0,
'user': 0.0},
'system': 1.0,
'total': 61.0,
'user': 1.0},
{'cpu_number': 13,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 40.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1361,14 +1332,14 @@ Quicklook stats:
'nice': 0.0,
'softirq': 0.0,
'steal': 0.0,
'system': 0.0,
'total': 45.0,
'user': 0.0},
'system': 1.0,
'total': 60.0,
'user': 1.0},
{'cpu_number': 14,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 39.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1377,13 +1348,13 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 45.0,
'user': 0.0},
'total': 61.0,
'user': 1.0},
{'cpu_number': 15,
'dpc': None,
'guest': 0.0,
'guest_nice': 0.0,
'idle': 55.0,
'idle': 40.0,
'interrupt': None,
'iowait': 0.0,
'irq': 0.0,
@ -1392,9 +1363,9 @@ Quicklook stats:
'softirq': 0.0,
'steal': 0.0,
'system': 1.0,
'total': 45.0,
'total': 60.0,
'user': 1.0}],
'swap': 100.0}
'swap': 1.9}
>>> gl.quicklook.keys()
['cpu_name', 'cpu_hz_current', 'cpu_hz', 'cpu', 'percpu', 'mem', 'swap', 'cpu_log_core', 'cpu_phys_core', 'load']
>>> gl.quicklook.get("cpu_name")
@ -1444,13 +1415,13 @@ Memswap stats:
>>> type(gl.memswap)
<class 'glances.plugins.memswap.MemswapPlugin'>
>>> gl.memswap
{'free': 1306624,
'percent': 100.0,
'sin': 2306797568,
'sout': 7715819520,
'time_since_update': 0.7321233749389648,
{'free': 4211441664,
'percent': 1.9,
'sin': 2388013056,
'sout': 7785721856,
'time_since_update': 0.35303711891174316,
'total': 4294963200,
'used': 4293656576}
'used': 83521536}
>>> gl.memswap.keys()
['total', 'used', 'free', 'percent', 'sin', 'sout', 'time_since_update']
>>> gl.memswap.get("total")
@ -1485,10 +1456,10 @@ Use auto_unit() function to generate a human-readable string with the unit:
.. code-block:: python
>>> gl.mem.get("used")
12810799128
10525812760
>>> gl.auto_unit(gl.mem.get("used"))
11.9G
9.80G
Args:
@ -1514,7 +1485,7 @@ Use bar() function to generate a bar:
.. code-block:: python
>>> gl.bar(gl.mem["percent"])
■■■■■■■■■■■■■■□□□□
■■■■■■■■■■■□□□□□□□
Args:
@ -1544,7 +1515,7 @@ Use top_process() function to generate a list of top processes sorted by CPU or
.. code-block:: python
>>> gl.top_process()
[{'status': 'S', 'memory_info': {'rss': 833155072, 'vms': 1499914432512, 'shared': 37560320, 'text': 148733952, 'lib': 0, 'data': 2573008896, 'dirty': 0}, 'name': 'code', 'nice': 0, 'memory_percent': 5.073152895924881, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'num_threads': 22, 'io_counters': [1554405376, 611483648, 1554405376, 611483648, 1], 'pid': 12321, 'cpu_percent': 1.6, 'cpu_times': {'user': 1797.6, 'system': 767.31, 'children_user': 1013.92, 'children_system': 950.58, 'iowait': 0.0}, 'key': 'pid', 'time_since_update': 0.6452903747558594, 'cmdline': ['/proc/self/exe', '--type=utility', '--utility-sub-type=node.mojom.NodeService', '--lang=en-US', '--service-sandbox-type=none', '--no-sandbox', '--dns-result-order=ipv4first', '--experimental-network-inspection', '--inspect-port=0', '--crashpad-handler-pid=12138', '--enable-crash-reporter=864d4bb7-dd20-4851-830f-29e81dd93517,no_channel', '--user-data-dir=/home/nicolargo/.config/Code', '--standard-schemes=vscode-webview,vscode-file', '--secure-schemes=vscode-webview,vscode-file', '--cors-schemes=vscode-webview,vscode-file', '--fetch-schemes=vscode-webview,vscode-file', '--service-worker-schemes=vscode-webview', '--code-cache-schemes=vscode-webview,vscode-file', '--shared-files=v8_context_snapshot_data:100', '--field-trial-handle=3,i,768474601394521582,1148810169323883902,262144', '--enable-features=DocumentPolicyIncludeJSCallStacksInCrashReports,EarlyEstablishGpuChannel,EstablishGpuChannelAsync', '--disable-features=CalculateNativeWinOcclusion,FontationsLinuxSystemFonts,ScreenAIOCREnabled,SpareRendererForSitePerProcess', '--variations-seed-version'], 'username': 'nicolargo'}, {'status': 'S', 'memory_info': {'rss': 610873344, 'vms': 31620141056, 'shared': 180371456, 'text': 708608, 'lib': 0, 'data': 1298669568, 'dirty': 0}, 'name': 'firefox', 'nice': 0, 'memory_percent': 3.71966033492132, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'num_threads': 157, 'io_counters': [1705658368, 4228337664, 1705658368, 4228337664, 1], 'pid': 471623, 'cpu_percent': 1.6, 'cpu_times': {'user': 2637.37, 'system': 751.51, 'children_user': 0.13, 'children_system': 1.18, 'iowait': 0.0}, 'key': 'pid', 'time_since_update': 0.6452903747558594, 'cmdline': ['/snap/firefox/7559/usr/lib/firefox/firefox'], 'username': 'nicolargo'}, {'status': 'S', 'memory_info': {'rss': 478920704, 'vms': 4367515648, 'shared': 76931072, 'text': 708608, 'lib': 0, 'data': 692023296, 'dirty': 0}, 'name': 'Isolated Web Co', 'nice': 0, 'memory_percent': 2.9161893602635156, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'num_threads': 33, 'io_counters': [38509568, 0, 38509568, 0, 1], 'pid': 472861, 'cpu_percent': 1.6, 'cpu_times': {'user': 925.58, 'system': 94.68, 'children_user': 0.0, 'children_system': 0.0, 'iowait': 0.0}, 'key': 'pid', 'time_since_update': 0.6452903747558594, 'cmdline': ['/snap/firefox/7559/usr/lib/firefox/firefox', '-contentproc', '-isForBrowser', '-prefsHandle', '0:45904', '-prefMapHandle', '1:280269', '-jsInitHandle', '2:223356', '-parentBuildID', '20251217233610', '-sandboxReporter', '3', '-chrootClient', '4', '-ipcHandle', '5', '-initialChannelId', '{b750b1b5-b51f-4cbd-8493-9c918fb23a11}', '-parentPid', '471623', '-crashReporter', '6', '-crashHelper', '7', '-greomni', '/snap/firefox/7559/usr/lib/firefox/omni.ja', '-appomni', '/snap/firefox/7559/usr/lib/firefox/browser/omni.ja', '-appDir', '/snap/firefox/7559/usr/lib/firefox/browser', '11', 'tab'], 'username': 'nicolargo'}]
[{'pid': 817540, 'memory_percent': 2.7617802712023183, 'status': 'S', 'num_threads': 25, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'io_counters': [23214080, 8192, 23214080, 8192, 1], 'cpu_times': {'user': 501.78, 'system': 42.5, 'children_user': 0.0, 'children_system': 0.0, 'iowait': 0.0}, 'nice': 0, 'cpu_percent': 5.2, 'name': 'code', 'memory_info': {'rss': 453562368, 'vms': 1517460189184, 'shared': 143192064, 'text': 148733952, 'lib': 0, 'data': 1318674432, 'dirty': 0}, 'key': 'pid', 'time_since_update': 0.40526819229125977, 'cmdline': ['/snap/code/211/usr/share/code/code', '--type=zygote', '--no-sandbox'], 'username': 'nicolargo'}, {'pid': 814875, 'memory_percent': 4.359469147986559, 'status': 'S', 'num_threads': 146, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'io_counters': [778276864, 563982336, 778276864, 563982336, 1], 'cpu_times': {'user': 154.0, 'system': 52.27, 'children_user': 0.09, 'children_system': 0.41, 'iowait': 0.0}, 'nice': 0, 'cpu_percent': 2.6, 'name': 'firefox', 'memory_info': {'rss': 715948032, 'vms': 22647500800, 'shared': 272883712, 'text': 708608, 'lib': 0, 'data': 1006481408, 'dirty': 0}, 'key': 'pid', 'time_since_update': 0.40526819229125977, 'cmdline': ['/snap/firefox/7559/usr/lib/firefox/firefox'], 'username': 'nicolargo'}, {'pid': 813963, 'memory_percent': 2.5835526261641473, 'status': 'S', 'num_threads': 24, 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, 'io_counters': [52350976, 73728, 52350976, 73728, 1], 'cpu_times': {'user': 183.97, 'system': 98.37, 'children_user': 1.44, 'children_system': 1.33, 'iowait': 0.0}, 'nice': 0, 'cpu_percent': 2.6, 'name': 'gnome-shell', 'memory_info': {'rss': 424292352, 'vms': 5059411968, 'shared': 166699008, 'text': 8192, 'lib': 0, 'data': 479715328, 'dirty': 0}, 'key': 'pid', 'time_since_update': 0.40526819229125977, 'cmdline': ['/usr/bin/gnome-shell'], 'username': 'nicolargo'}]
Args:

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "GLANCES" "1" "Jan 03, 2026" "4.4.2_dev1" "Glances"
.TH "GLANCES" "1" "Jan 04, 2026" "4.4.2_dev1" "Glances"
.SH NAME
glances \- An eye on your system
.SH SYNOPSIS

104
glances/jwt_utils.py Normal file
View File

@ -0,0 +1,104 @@
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2026 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""JWT utilities for Glances REST API authentication."""
import secrets
from datetime import datetime, timedelta, timezone
from glances.logger import logger
# JWT library import with fallback
try:
from jose import JWTError, jwt
JWT_AVAILABLE = True
except ImportError:
JWT_AVAILABLE = False
JWTError = Exception # Placeholder
class JWTHandler:
"""Handle JWT token creation and validation."""
# Algorithm for JWT signing
ALGORITHM = "HS256"
def __init__(self, secret_key: str | None = None, expire_minutes: int = 60):
"""Initialize JWT handler.
Args:
secret_key: Secret key for signing tokens. If None, generates a random key.
expire_minutes: Token expiration time in minutes (default: 60)
"""
if not JWT_AVAILABLE:
logger.warning("python-jose library not available. JWT authentication disabled.")
self._secret_key = None
else:
# Use provided key or generate a secure random key
self._secret_key = secret_key or secrets.token_urlsafe(32)
if secret_key is None:
logger.info("JWT secret key generated (valid for this server instance only)")
self._expire_minutes = expire_minutes
@property
def is_available(self) -> bool:
"""Check if JWT functionality is available."""
return JWT_AVAILABLE and self._secret_key is not None
@property
def expire_minutes(self) -> int:
"""Return the token expiration time in minutes."""
return self._expire_minutes
def create_access_token(self, username: str) -> str:
"""Create a JWT access token for the given username.
Args:
username: The username to encode in the token
Returns:
Encoded JWT token string
Raises:
RuntimeError: If JWT is not available
"""
if not self.is_available:
raise RuntimeError("JWT authentication is not available")
expire = datetime.now(timezone.utc) + timedelta(minutes=self._expire_minutes)
to_encode = {
"sub": username,
"exp": expire,
"iat": datetime.now(timezone.utc),
"iss": "glances",
}
return jwt.encode(to_encode, self._secret_key, algorithm=self.ALGORITHM)
def verify_token(self, token: str) -> str | None:
"""Verify a JWT token and extract the username.
Args:
token: The JWT token to verify
Returns:
Username if valid, None otherwise
"""
if not self.is_available:
return None
try:
payload = jwt.decode(token, self._secret_key, algorithms=[self.ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
return username
except JWTError as e:
logger.debug(f"JWT verification failed: {e}")
return None

View File

@ -427,12 +427,13 @@ Examples of use:
dest='bind_address',
help='bind server to the given IPv4/IPv6 address or hostname',
)
parser.add_argument('-u', dest='username_used', help='use or define the given username')
parser.add_argument(
'--username',
action='store_true',
default=False,
dest='username_prompt',
help='define a client/server username',
help='define or use an username',
)
parser.add_argument(
'--password',
@ -441,7 +442,6 @@ Examples of use:
dest='password_prompt',
help='define a client/server password',
)
parser.add_argument('-u', dest='username_used', help='use the given client/server username')
parser.add_argument('--snmp-community', default='public', dest='snmp_community', help='SNMP community')
parser.add_argument('--snmp-port', default=161, type=int, dest='snmp_port', help='SNMP port')
parser.add_argument('--snmp-version', default='2c', dest='snmp_version', help='SNMP version (1, 2c or 3)')
@ -775,12 +775,10 @@ Examples of use:
# Every username needs a password
args.password_prompt = True
# Prompt username
if args.server:
args.username = self.__get_username(description='Define the Glances server username: ')
elif args.webserver:
args.username = self.__get_username(description='Define the Glances webserver username: ')
if args.server or args.webserver:
args.username = self.__get_username(description='Enter new username: ')
elif args.client:
args.username = self.__get_username(description='Enter the Glances server username: ')
args.username = self.__get_username(description='Enter username: ')
else:
if args.username_used:
# A username has been set using the -u option ?
@ -791,21 +789,15 @@ Examples of use:
if args.password_prompt or args.username_used:
# Interactive or file password
if args.server:
if args.server or args.webserver:
args.password = self.__get_password(
description=f'Define the Glances server password ({args.username} username): ',
confirm=True,
username=args.username,
)
elif args.webserver:
args.password = self.__get_password(
description=f'Define the Glances webserver password ({args.username} username): ',
description='Enter new password: ',
confirm=True,
username=args.username,
)
elif args.client:
args.password = self.__get_password(
description=f'Enter the Glances server password ({args.username} username): ',
description='Enter password: ',
clear=True,
username=args.username,
)

View File

@ -20,6 +20,15 @@ from glances.events_list import glances_events
from glances.globals import json_dumps
from glances.logger import logger
from glances.password import GlancesPassword
# JWT import with fallback
try:
from glances.jwt_utils import JWTHandler
JWT_AVAILABLE = True
except ImportError:
JWT_AVAILABLE = False
JWTHandler = None
from glances.plugins.plugin.dag import get_plugin_dependencies
from glances.processes import glances_processes
from glances.servers_list import GlancesServersList
@ -33,7 +42,7 @@ try:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.security import HTTPAuthorizationCredentials, HTTPBasic, HTTPBasicCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
except ImportError as e:
@ -50,7 +59,8 @@ import contextlib
import threading
import time
security = HTTPBasic()
security = HTTPBasic(auto_error=False)
bearer_security = HTTPBearer(auto_error=False)
class GlancesJSONResponse(JSONResponse):
@ -122,13 +132,23 @@ class GlancesRestfulApi:
self.bind_url = urljoin(f'{self.protocol}://{self.args.bind_address}:{self.args.port}/', self.url_prefix)
# FastAPI Init
# Note: Authentication is now applied at router level, not app level,
# to allow the token endpoint to be unauthenticated
self._app = FastAPI(default_response_class=GlancesJSONResponse)
if self.args.password:
self._app = FastAPI(default_response_class=GlancesJSONResponse, dependencies=[Depends(self.authentication)])
self._password = GlancesPassword(username=args.username, config=config)
# Initialize JWT handler
if JWT_AVAILABLE:
jwt_secret = config.get_value('outputs', 'jwt_secret_key', default=None)
jwt_expire = config.get_int_value('outputs', 'jwt_expire_minutes', default=60)
self._jwt_handler = JWTHandler(secret_key=jwt_secret, expire_minutes=jwt_expire)
logger.info(f"JWT authentication enabled (token expiration: {jwt_expire} minutes)")
else:
self._jwt_handler = None
logger.info("JWT authentication not available (python-jose not installed)")
else:
self._app = FastAPI(default_response_class=GlancesJSONResponse)
self._password = None
self._jwt_handler = None
# Set path for WebUI
webui_root_path = config.get_value(
@ -158,6 +178,9 @@ class GlancesRestfulApi:
)
# FastAPI Define routes
# Token endpoint router (no authentication required) - must be added first
if self.args.password and self._jwt_handler is not None:
self._app.include_router(self._token_router())
self._app.include_router(self._router())
# Enable auto discovering of the service
@ -206,16 +229,36 @@ class GlancesRestfulApi:
self.servers_list.update_servers_stats()
self.timer = Timer(self.args.cached_time)
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
"""Check if a username/password combination is valid."""
if creds.username == self.args.username:
# check_password
if self._password.check_password(self.args.password, self._password.get_hash(creds.password)):
return creds.username
def authentication(
self,
basic_creds: Annotated[HTTPBasicCredentials | None, Depends(security)] = None,
bearer_creds: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer_security)] = None,
):
"""Check if a username/password combination or JWT token is valid.
# If the username/password combination is invalid, return an HTTP 401
Supports both HTTP Basic Auth and Bearer Token (JWT) authentication.
"""
# Try JWT Bearer token first
if bearer_creds is not None and self._jwt_handler is not None:
username = self._jwt_handler.verify_token(bearer_creds.credentials)
if username is not None:
# Verify the username matches the configured username
if username == self.args.username:
return username
logger.warning(f"JWT token contains unknown username: {username}")
# Fall back to Basic Auth
if basic_creds is not None:
if basic_creds.username == self.args.username:
if self._password.check_password(self.args.password, self._password.get_hash(basic_creds.password)):
return basic_creds.username
# If no valid authentication provided, return HTTP 401
www_authenticate = "Bearer, Basic" if self._jwt_handler and self._jwt_handler.is_available else "Basic"
raise HTTPException(
status.HTTP_401_UNAUTHORIZED, "Incorrect username or password", {"WWW-Authenticate": "Basic"}
status.HTTP_401_UNAUTHORIZED,
"Incorrect authentication",
{"WWW-Authenticate": www_authenticate},
)
def _logo(self):
@ -228,13 +271,24 @@ class GlancesRestfulApi:
\_____|_|\__,_|_| |_|\___\___||___/ {__version__}
"""
def _token_router(self) -> APIRouter:
"""Define a router for the token endpoint (no authentication required)."""
base_path = f'/api/{self.API_VERSION}'
router = APIRouter(prefix=self.url_prefix)
# Override global dependencies with empty list to disable authentication for this route
router.add_api_route(f'{base_path}/token', self._api_token, methods=['POST'], dependencies=[])
return router
def _router(self) -> APIRouter:
"""Define a custom router for Glances path."""
base_path = f'/api/{self.API_VERSION}'
plugin_path = f"{base_path}/{{plugin}}"
# Create the main router
router = APIRouter(prefix=self.url_prefix)
# Create the main router with authentication if password is set
if self.args.password:
router = APIRouter(prefix=self.url_prefix, dependencies=[Depends(self.authentication)])
else:
router = APIRouter(prefix=self.url_prefix)
# REST API route definition
# ==========================
@ -426,6 +480,80 @@ class GlancesRestfulApi:
glances_events.clean(critical=True)
return GlancesJSONResponse({})
async def _api_token(self, request: Request):
"""Glances API RESTful implementation.
Generate a JWT access token for authenticated users.
Expected JSON body:
{
"username": "string",
"password": "string"
}
Returns:
{
"access_token": "string",
"token_type": "bearer",
"expires_in": int (seconds)
}
"""
# Check if JWT is available
if self._jwt_handler is None or not self._jwt_handler.is_available:
raise HTTPException(
status.HTTP_501_NOT_IMPLEMENTED,
"JWT authentication is not available. Install python-jose or check configuration.",
)
# Check if password authentication is enabled
if self._password is None:
raise HTTPException(
status.HTTP_501_NOT_IMPLEMENTED,
"Password authentication is not enabled. Start Glances with --password option.",
)
# Parse request body
try:
body = await request.json()
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid JSON body")
username = body.get('username')
password = body.get('password')
if not username or not password:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Missing username or password in request body",
)
# Validate credentials
if username != self.args.username:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
"Incorrect authentication",
{"WWW-Authenticate": "Bearer"},
)
# Check password
if not self._password.check_password(self.args.password, self._password.get_hash(password)):
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
"Incorrect authentication",
{"WWW-Authenticate": "Bearer"},
)
# Generate token
access_token = self._jwt_handler.create_access_token(username)
return GlancesJSONResponse(
{
"access_token": access_token,
"token_type": "bearer",
"expires_in": self._jwt_handler.expire_minutes * 60,
}
)
def _api_help(self):
"""Glances API RESTful implementation.

View File

@ -60,6 +60,7 @@ Note: The url_prefix should always end with a slash (``/``).
For example:
.. code-block:: ini
[outputs]
url_prefix = /glances/
@ -72,6 +73,97 @@ API documentation URL
The API documentation is embeded in the server and available at the following URL:
``http://localhost:61208/docs#/``.
Authentication
--------------
Glances API supports both HTTP Basic authentication and JWT (JSON Web Token) Bearer authentication.
To enable authentication, start Glances with the ``--password`` option.
To generate a new login/password pair, use the following command:
.. code-block:: bash
glances -w --username
> Enter new username: foo
> Enter new password: ********
> Confirm new password: ********
> User 'username' created/updated successfully.
To reuse an existing login/password pair, start Glances with the ``-u <user>`` option:
.. code-block:: bash
glances -w -u foo
JWT Token Authentication
~~~~~~~~~~~~~~~~~~~~~~~~
JWT authentication requires the ``python-jose`` library to be installed.
**Step 1: Get a JWT Token**
Request a token by sending your credentials to the token endpoint:
.. code-block:: bash
curl -X POST http://localhost:61208/api/{__apiversion__}/token \\
-H "Content-Type: application/json" \\
-d '{{"username": "your_username", "password": "your_password"}}'
This will return a response like:
.. code-block:: json
{{
"access_token": "...",
"token_type": "bearer",
"expires_in": 3600
}}
**Step 2: Use the Token**
Use the token in the Authorization header with Bearer authentication:
.. code-block:: bash
# Store the token in a variable
TOKEN="your_access_token_here"
# Access a protected endpoint
curl -H "Authorization: Bearer $TOKEN" \\
http://localhost:61208/api/{__apiversion__}/cpu
**Complete Example:**
.. code-block:: bash
# Get token and extract access_token
TOKEN=$(curl -s -X POST http://localhost:61208/api/{__apiversion__}/token \\
-H "Content-Type: application/json" \\
-d '{{"username": "glances", "password": "mypassword"}}' \\
| grep -o '"access_token":"[^"]*"' \\
| cut -d'"' -f4)
# Use the token to get CPU stats
curl -H "Authorization: Bearer $TOKEN" \\
http://localhost:61208/api/{__apiversion__}/cpu
**Configuration:**
You can configure JWT settings in the Glances configuration file:
.. code-block:: ini
[outputs]
# JWT secret key (if not set, a random key will be generated)
jwt_secret_key = your-secret-key-here
# JWT token expiration in minutes (default: 60)
jwt_expire_minutes = 60
**Note:** The token endpoint (``/api/{__apiversion__}/token``) does not require authentication.
Protected endpoints support both Bearer token and Basic Auth authentication methods.
WebUI refresh
-------------

View File

@ -85,7 +85,7 @@ class GlancesPassword:
password_hashed = self.hash_password(password_hash)
if confirm:
# password_confirm is the clear password (only used to compare)
password_confirm = self.get_hash(getpass.getpass('Password (confirm): '))
password_confirm = self.get_hash(getpass.getpass('Confirm new password: '))
if not self.check_password(password_hashed, password_confirm):
logger.critical("Sorry, passwords do not match. Exit.")
@ -99,7 +99,7 @@ class GlancesPassword:
# Save the hashed password to the password file
if not clear:
save_input = input('Do you want to save the password? [Yes/No]: ')
save_input = input(f'Do you want to save the password in {self.password_file} ? [Yes/No]: ')
if save_input and save_input[0].upper() == 'Y':
self.save_password(password_hashed)

View File

@ -102,6 +102,7 @@ snmp = ["pysnmp-lextudio<6.2.0"]
sparklines = ["sparklines"]
web = [
"fastapi>=0.82.0",
"python-jose[cryptography]>=3.3.0",
"requests",
"uvicorn",
]