nodeocc 1.0.25__tar.gz → 1.0.26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nodeocc-1.0.25 → nodeocc-1.0.26}/PKG-INFO +1 -1
- {nodeocc-1.0.25 → nodeocc-1.0.26}/pyproject.toml +1 -1
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/controller/controller.py +29 -29
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/PKG-INFO +1 -1
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/readers/slurmreader.py +16 -6
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/__init__.py +7 -1
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/curses_multiwindow.py +17 -6
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/slurm_list.py +1 -1
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/slurm_viz.py +21 -6
- {nodeocc-1.0.25 → nodeocc-1.0.26}/README.md +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/setup.cfg +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/setup.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/__init__.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/model/infrastructure.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/model/job.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/SOURCES.txt +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/dependency_links.txt +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/entry_points.txt +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/requires.txt +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/nodeocc.egg-info/top_level.txt +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/readers/compute_wait_time.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/styles.py +0 -0
- {nodeocc-1.0.25 → nodeocc-1.0.26}/src/view/utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nodeocc"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.26"
|
|
8
8
|
authors = [{ name = "Matteo Boschini" }]
|
|
9
9
|
maintainers = [{ name = "Lorenzo Bonicelli" }, { name = "Aniello Panariello" }]
|
|
10
10
|
description = "TUI application for viewing the status of GPU allocations on a Slurm cluster"
|
|
@@ -24,6 +24,8 @@ import json
|
|
|
24
24
|
import argparse
|
|
25
25
|
import traceback
|
|
26
26
|
import socket
|
|
27
|
+
import re
|
|
28
|
+
from urllib.request import urlopen
|
|
27
29
|
from model.infrastructure import Infrastructure
|
|
28
30
|
from model.job import Job
|
|
29
31
|
import asyncio
|
|
@@ -31,36 +33,28 @@ import importlib.metadata
|
|
|
31
33
|
|
|
32
34
|
program_name = 'nodeocc'
|
|
33
35
|
version_number = importlib.metadata.version(program_name)
|
|
34
|
-
updated = False
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
updated = True
|
|
58
|
-
time.sleep(3)
|
|
59
|
-
print(END, end='')
|
|
60
|
-
except Exception as e:
|
|
61
|
-
print(f"{BBLUE}Could not get newest version from pip, check if {program_name} is up to date{END}")
|
|
62
|
-
newest_version = '?.?.?'
|
|
63
|
-
time.sleep(5)
|
|
37
|
+
|
|
38
|
+
def _version_tuple(v):
|
|
39
|
+
return tuple(int(x) for x in re.findall(r'\d+', str(v)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_latest_version(cur_version, timeout_sec=1.2):
|
|
43
|
+
"""
|
|
44
|
+
Fast, non-blocking-ish version check:
|
|
45
|
+
- short timeout
|
|
46
|
+
- no startup sleeps/pauses
|
|
47
|
+
- graceful fallback to current version on errors
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
url = f"https://pypi.python.org/pypi/{program_name}/json"
|
|
51
|
+
with urlopen(url, timeout=timeout_sec) as response:
|
|
52
|
+
payload = json.loads(response.read().decode('utf-8'))
|
|
53
|
+
newest = payload['info']['version']
|
|
54
|
+
is_updated = _version_tuple(cur_version) >= _version_tuple(newest)
|
|
55
|
+
return newest, is_updated
|
|
56
|
+
except Exception:
|
|
57
|
+
return cur_version, True
|
|
64
58
|
|
|
65
59
|
BEGIN_DELIM = "!{$"
|
|
66
60
|
END_DELIM_ENCODED = "!}$".encode('utf-8')
|
|
@@ -81,6 +75,12 @@ args = parser.parse_args()
|
|
|
81
75
|
# check basepath is readable
|
|
82
76
|
assert os.access(args.basepath, os.R_OK), f"Base path {args.basepath} not readable"
|
|
83
77
|
|
|
78
|
+
# Keep startup snappy for daemon/diagnostics; check updates only in interactive mode.
|
|
79
|
+
if args.master_only or args.diag:
|
|
80
|
+
newest_version, updated = version_number, True
|
|
81
|
+
else:
|
|
82
|
+
newest_version, updated = check_latest_version(version_number)
|
|
83
|
+
|
|
84
84
|
# export args
|
|
85
85
|
orig_instance = Singleton.getInstance(args)
|
|
86
86
|
|
|
@@ -57,9 +57,16 @@ def _parse_gpu_count(gres):
|
|
|
57
57
|
token = token.strip()
|
|
58
58
|
if not token.lower().startswith('gpu'):
|
|
59
59
|
continue
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
token_core = token.split('(')[0]
|
|
61
|
+
parts = token_core.split(':')
|
|
62
|
+
count = None
|
|
63
|
+
|
|
64
|
+
if len(parts) >= 2 and re.fullmatch(r'\d+', parts[-1]):
|
|
65
|
+
count = int(parts[-1])
|
|
66
|
+
elif len(parts) >= 3 and re.fullmatch(r'\d+', parts[-2]):
|
|
67
|
+
count = int(parts[-2])
|
|
68
|
+
|
|
69
|
+
n_gpu += count if count is not None else 1
|
|
63
70
|
return n_gpu
|
|
64
71
|
|
|
65
72
|
|
|
@@ -75,10 +82,13 @@ def _parse_gpu_models(gres):
|
|
|
75
82
|
token = token.strip()
|
|
76
83
|
if not token.lower().startswith('gpu'):
|
|
77
84
|
continue
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
token_core = token.split('(')[0]
|
|
86
|
+
parts = token_core.split(':')
|
|
87
|
+
if len(parts) >= 3 and not re.fullmatch(r'\d+', parts[1]):
|
|
88
|
+
model = parts[1]
|
|
89
|
+
elif len(parts) == 2 and not re.fullmatch(r'\d+', parts[1]):
|
|
80
90
|
model = parts[1]
|
|
81
|
-
elif len(parts) == 2:
|
|
91
|
+
elif len(parts) == 2 and re.fullmatch(r'\d+', parts[1]):
|
|
82
92
|
model = 'gpu'
|
|
83
93
|
else:
|
|
84
94
|
model = 'n/a'
|
|
@@ -13,5 +13,11 @@ def update_views(stdscr, instance, filter):
|
|
|
13
13
|
instance, 'job_id_type') else 'agg')
|
|
14
14
|
instance.timeme(f"- print job list")
|
|
15
15
|
|
|
16
|
-
instance.nocc = slurm_viz.view_viz(
|
|
16
|
+
instance.nocc = slurm_viz.view_viz(
|
|
17
|
+
instance.inf,
|
|
18
|
+
instance.jobs,
|
|
19
|
+
stylefn=styles.crsstyler,
|
|
20
|
+
mode=instance.view_mode if hasattr(instance, 'view_mode') else 'gpu',
|
|
21
|
+
panel_width=instance.right_width if hasattr(instance, 'right_width') else None,
|
|
22
|
+
)
|
|
17
23
|
instance.timeme(f"- view nodeocc")
|
|
@@ -35,6 +35,11 @@ BUTTON_ACTIONS = {
|
|
|
35
35
|
'info': [ord('i')],
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
RIGHT_WIDTH_GPU = 33
|
|
39
|
+
RIGHT_WIDTH_INFO = 39
|
|
40
|
+
# Keep a single stable width across modes; use the widest one so info fields are not cut.
|
|
41
|
+
RIGHT_WIDTH_UNIFIED = max(RIGHT_WIDTH_GPU, RIGHT_WIDTH_INFO)
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
def is_file_writable_byall(file_path):
|
|
40
45
|
return os.stat(file_path).st_mode & stat.S_IWOTH
|
|
@@ -476,7 +481,7 @@ def handle_keys(stdscr, instance):
|
|
|
476
481
|
instance.sort_by_prio = not instance.sort_by_prio
|
|
477
482
|
elif k in BUTTON_ACTIONS['res_view_mode']:
|
|
478
483
|
instance.view_mode = {"gpu": "ram", "ram": "cpu", "cpu": "gpu", "info": "gpu"}[instance.view_mode]
|
|
479
|
-
instance.right_width =
|
|
484
|
+
instance.right_width = RIGHT_WIDTH_UNIFIED
|
|
480
485
|
stdscr.clear()
|
|
481
486
|
elif k in BUTTON_ACTIONS['job_id_type']:
|
|
482
487
|
instance.job_id_type = "true" if instance.job_id_type == "agg" else "agg"
|
|
@@ -489,7 +494,7 @@ def handle_keys(stdscr, instance):
|
|
|
489
494
|
instance.show_prio = not instance.show_prio
|
|
490
495
|
elif k in BUTTON_ACTIONS['info']:
|
|
491
496
|
instance.view_mode = 'info' if instance.view_mode != 'info' else 'gpu'
|
|
492
|
-
instance.right_width =
|
|
497
|
+
instance.right_width = RIGHT_WIDTH_UNIFIED
|
|
493
498
|
stdscr.clear()
|
|
494
499
|
elif k in BUTTON_ACTIONS['up_tab']:
|
|
495
500
|
# get screen size
|
|
@@ -513,7 +518,7 @@ def update_screen(stdscr, instance):
|
|
|
513
518
|
if instance.k in BUTTON_ACTIONS['refresh']:
|
|
514
519
|
stdscr.clear()
|
|
515
520
|
|
|
516
|
-
totsize = 106
|
|
521
|
+
totsize = 106 + max(0, RIGHT_WIDTH_UNIFIED - RIGHT_WIDTH_GPU)
|
|
517
522
|
if instance.show_account:
|
|
518
523
|
totsize += 10
|
|
519
524
|
if instance.show_prio:
|
|
@@ -606,8 +611,14 @@ def update_screen(stdscr, instance):
|
|
|
606
611
|
# get slurm user partition
|
|
607
612
|
# stdscr.addstr(lines - 1, xoffset + 62 + 2, f'(Avg time {instance.avg_wait_time})', curses.color_pair(2))
|
|
608
613
|
|
|
609
|
-
|
|
610
|
-
|
|
614
|
+
if instance.updated:
|
|
615
|
+
signature = instance.signature
|
|
616
|
+
sig_style = curses.color_pair(2)
|
|
617
|
+
else:
|
|
618
|
+
signature = f'{instance.signature} (new {instance.newest_version})'
|
|
619
|
+
sig_style = curses.color_pair(4)
|
|
620
|
+
|
|
621
|
+
stdscr.addstr(lines - 1, columns - 2 - len(signature), signature, sig_style)
|
|
611
622
|
|
|
612
623
|
stdscr.refresh()
|
|
613
624
|
curses.doupdate()
|
|
@@ -712,7 +723,7 @@ async def main(stdscr):
|
|
|
712
723
|
# status
|
|
713
724
|
|
|
714
725
|
instance.a_filter = 0
|
|
715
|
-
instance.right_width =
|
|
726
|
+
instance.right_width = RIGHT_WIDTH_UNIFIED
|
|
716
727
|
|
|
717
728
|
stdscr.clear()
|
|
718
729
|
|
|
@@ -44,7 +44,7 @@ def _joblet_format(instance, job, width=74, jobid_type='agg'):
|
|
|
44
44
|
mem = round(joblet.mem / 1024)
|
|
45
45
|
joblet_repr += _format_to(f'{mem}G', 4, 'right')
|
|
46
46
|
joblet_repr += ' '
|
|
47
|
-
if instance.view_mode in ('gpu', 'ram'):
|
|
47
|
+
if instance.view_mode in ('gpu', 'ram', 'info'):
|
|
48
48
|
joblet_repr += _format_to((f'{joblet.n_gpus}gp' if joblet.n_gpus > 0 else " - "), 3, 'left')
|
|
49
49
|
elif instance.view_mode == 'cpu':
|
|
50
50
|
joblet_repr += _format_to((f'{joblet.cpus}cp' if joblet.cpus > 0 else " - "), 3, 'left')
|
|
@@ -101,6 +101,20 @@ cpu_paused = '▚'
|
|
|
101
101
|
cpu_down = '⨯'
|
|
102
102
|
|
|
103
103
|
GLOBAL_NODES = None
|
|
104
|
+
BASE_PANEL_WIDTH = 33
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _center_panel_content(text, panel_width, base_width=BASE_PANEL_WIDTH):
|
|
108
|
+
if panel_width is None or panel_width <= base_width:
|
|
109
|
+
return text
|
|
110
|
+
|
|
111
|
+
pad = max(0, (panel_width - base_width) // 2)
|
|
112
|
+
if pad == 0:
|
|
113
|
+
return text
|
|
114
|
+
|
|
115
|
+
lines = text.split('\n')
|
|
116
|
+
lines = [(' ' * pad + line) if line else line for line in lines]
|
|
117
|
+
return '\n'.join(lines)
|
|
104
118
|
|
|
105
119
|
|
|
106
120
|
def get_ram_block(megs):
|
|
@@ -138,15 +152,15 @@ def get_node_status(node, jobs):
|
|
|
138
152
|
return None # white?
|
|
139
153
|
|
|
140
154
|
|
|
141
|
-
def view_viz(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, mode='gpu'):
|
|
155
|
+
def view_viz(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, mode='gpu', panel_width=None):
|
|
142
156
|
if mode == 'gpu':
|
|
143
|
-
return view_viz_gpu(infrastructure, jobs, work, stylefn, current_user)
|
|
157
|
+
return _center_panel_content(view_viz_gpu(infrastructure, jobs, work, stylefn, current_user), panel_width)
|
|
144
158
|
elif mode == 'ram':
|
|
145
|
-
return view_viz_ram(infrastructure, jobs, work, stylefn, current_user)
|
|
159
|
+
return _center_panel_content(view_viz_ram(infrastructure, jobs, work, stylefn, current_user), panel_width)
|
|
146
160
|
elif mode == 'cpu':
|
|
147
|
-
return view_viz_cpu(infrastructure, jobs, work, stylefn, current_user)
|
|
161
|
+
return _center_panel_content(view_viz_cpu(infrastructure, jobs, work, stylefn, current_user), panel_width)
|
|
148
162
|
elif mode == 'info':
|
|
149
|
-
return view_viz_info(infrastructure, jobs, work, stylefn, current_user)
|
|
163
|
+
return view_viz_info(infrastructure, jobs, work, stylefn, current_user, panel_width)
|
|
150
164
|
|
|
151
165
|
|
|
152
166
|
def view_viz_ram(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None):
|
|
@@ -519,7 +533,7 @@ def view_viz_cpu(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user
|
|
|
519
533
|
return RetScope.return_string
|
|
520
534
|
|
|
521
535
|
|
|
522
|
-
def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None):
|
|
536
|
+
def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_user=None, panel_width=None):
|
|
523
537
|
# this is for hot reload
|
|
524
538
|
if not work:
|
|
525
539
|
return "UPDATE IN PROGRESS - PLZ W8 M8 B8"
|
|
@@ -555,6 +569,7 @@ def view_viz_info(infrastructure, jobs, work=True, stylefn=cmdstyle, current_use
|
|
|
555
569
|
# max_ram_len = max([len(str(n.mem // 1024)) for n in nodes])
|
|
556
570
|
feat_str = stylefn("GREEN", "FEATURES")
|
|
557
571
|
RetScope.return_string += f'{"":{gpu_name_chars}} GPU {feat_str}\n'
|
|
572
|
+
|
|
558
573
|
for i, n in GLOBAL_NODES.iterrows():
|
|
559
574
|
gpu_model = n.m_gpu # if n.m_gpu is not None else 'N/A'
|
|
560
575
|
# gpu_mem = n.gpu_mem if n.gpu_mem is not None else 'N/A'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|