@@ -182,7 +182,46 @@ def build_dataset_tree(datasets: List[Dataset]) -> List[Dataset]:
182182
183183 return roots
184184
185+ def parse_iostat_line (line : str ) -> Optional [tuple [str , str , int , int , int , int ]]:
186+ """
187+ Parses a single line from `zpool iostat -H -p -y 1`.
188+ Format: pool_name alloc free read_ops write_ops read_bytes write_bytes
189+ Or: vdev_name alloc free read_ops write_ops read_bytes write_bytes
190+
191+ Returns: (name, type_hint, read_ops, write_ops, read_bytes, write_bytes)
192+ type_hint is 'pool' or 'vdev' based on context (caller needs to track).
193+ Actually, `zpool iostat -H` prints flat lines.
194+ We need to know the structure.
195+ But usually it prints:
196+ pool_name ...
197+ vdev ...
198+
199+ With -H, indentation is preserved?
200+ Let's check `zpool iostat -H` behavior.
201+ Usually -H removes headers and separates with tabs.
202+ It DOES NOT preserve indentation usually.
203+ Wait, if indentation is lost, we can't distinguish pool from vdev easily if names are similar.
204+ However, the order is always Pool -> Vdevs.
205+
206+ Let's assume the caller handles the hierarchy or we just return the raw stats and name.
207+ """
208+ parts = line .strip ().split ('\t ' )
209+ if len (parts ) < 7 :
210+ return None
211+
212+ name = parts [0 ]
213+ try :
214+ # parts[1] alloc, parts[2] free
215+ read_ops = int (parts [3 ])
216+ write_ops = int (parts [4 ])
217+ read_bytes = int (parts [5 ])
218+ write_bytes = int (parts [6 ])
219+ return (name , read_ops , write_ops , read_bytes , write_bytes )
220+ except (ValueError , IndexError ):
221+ return None
222+
185223def parse_zpool_iostat (output : str ) -> Dict [str , Dict [str , dict ]]:
224+
186225 """
187226 Parses `zpool iostat -v -p` (using -p for exact numbers)
188227 Returns nested dict: pool -> vdev -> {read_ops, write_ops, read_bytes, write_bytes}
@@ -263,7 +302,11 @@ def parse_zpool_iostat(output: str) -> Dict[str, Dict[str, dict]]:
263302
264303 return stats
265304
266- def get_system_status () -> List [Pool ]:
305+ def get_static_data () -> List [Pool ]:
306+ """
307+ Fetches static structure: Pools, Vdevs, Datasets.
308+ Does NOT fetch realtime IO stats.
309+ """
267310 # 1. Get Pools
268311 pool_list_out = run_command (['zpool' , 'list' , '-H' , '-o' , 'name,size,alloc,free,frag,cap,health,altroot' ])
269312 pools = parse_zpool_list (pool_list_out )
@@ -280,38 +323,6 @@ def get_system_status() -> List[Pool]:
280323 snap_out = run_command (['zfs' , 'list' , '-H' , '-t' , 'snapshot' , '-o' , 'name,used' ])
281324 snaps_map = parse_zfs_snapshots (snap_out )
282325
283- # 5. Get IO Stats
284- # Use -p for parsable numbers, -v for verbose (vdevs)
285- # Note: `zpool iostat` without interval prints stats since boot.
286- # For realtime, we usually want current activity.
287- # `zpool iostat -v -p 1 2` prints 2 samples, first is since boot, second is last 1 sec.
288- # But that blocks for 1 second.
289- # If we want non-blocking, we have to accept "since boot" or manage a background process.
290- # Requirement: "Realtime Monitoring ... updated every 5 seconds".
291- # If we run `zpool iostat` every 5 seconds, we get "since boot" stats if we don't specify interval.
292- # If we specify interval, it blocks.
293- # Textual app is async, we could run it in a worker?
294- # Or we can just show "since boot" averages?
295- # Usually "Realtime" implies current load.
296- # Let's try to run `zpool iostat -v -p 1 1` which waits 1 second and gives 1 sample (actually 2 lines of headers + 2 samples).
297- # The second sample is the current load.
298- # But this adds 1s latency to the refresh.
299- # Let's use `zpool iostat -v -p -y 1 1`? -y omits first report (since boot) on recent ZFS versions.
300- # If -y is not supported, we parse the second data block.
301-
302- # Let's try `zpool iostat -v -p 1 1` and parse the last set of values.
303- # This will block for 1 second.
304- iostat_out = run_command (['zpool' , 'iostat' , '-v' , '-p' , '1' , '1' ])
305- # This command outputs:
306- # Header
307- # Stats since boot
308- # Header (sometimes)
309- # Stats for 1s
310-
311- # We need to parse this carefully.
312- # If we just take the last occurrence of each pool/vdev, it should be the latest sample.
313- io_stats = parse_zpool_iostat (iostat_out )
314-
315326 # Attach snapshots to datasets
316327 for ds in all_datasets :
317328 if ds .name in snaps_map :
@@ -325,23 +336,12 @@ def get_system_status() -> List[Pool]:
325336 if pool .name in vdevs_map :
326337 pool .vdevs = vdevs_map [pool .name ]
327338
328- # Attach IO Stats
329- if pool .name in io_stats :
330- p_stats = io_stats [pool .name ].get ("__pool__" , {})
331- pool .read_ops = p_stats .get ("read_ops" , 0 )
332- pool .write_ops = p_stats .get ("write_ops" , 0 )
333- pool .read_bytes = p_stats .get ("read_bytes" , 0 )
334- pool .write_bytes = p_stats .get ("write_bytes" , 0 )
335-
336- for vdev in pool .vdevs :
337- if vdev .name in io_stats [pool .name ]:
338- v_stats = io_stats [pool .name ][vdev .name ]
339- vdev .read_ops = v_stats .get ("read_ops" , 0 )
340- vdev .write_ops = v_stats .get ("write_ops" , 0 )
341- vdev .read_bytes = v_stats .get ("read_bytes" , 0 )
342- vdev .write_bytes = v_stats .get ("write_bytes" , 0 )
343-
344339 # Find root dataset for this pool
345340 pool .datasets = [ds for ds in root_datasets if ds .name == pool .name ]
346341
347342 return pools
343+
344+ def get_system_status () -> List [Pool ]:
345+ # Deprecated: Use get_static_data for structure and background thread for stats
346+ return get_static_data ()
347+
0 commit comments