nodeocc 1.0.11__tar.gz → 1.0.13__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.11 → nodeocc-1.0.13}/PKG-INFO +1 -1
- {nodeocc-1.0.11 → nodeocc-1.0.13}/pyproject.toml +1 -1
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/controller/controller.py +0 -13
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/PKG-INFO +1 -1
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/view/curses_multiwindow.py +72 -46
- {nodeocc-1.0.11 → nodeocc-1.0.13}/README.md +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/setup.cfg +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/setup.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/__init__.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/model/infrastructure.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/model/job.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/SOURCES.txt +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/dependency_links.txt +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/entry_points.txt +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/requires.txt +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/nodeocc.egg-info/top_level.txt +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/readers/compute_wait_time.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/readers/slurmreader.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/view/__init__.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/view/slurm_list.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/view/slurm_viz.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/src/view/styles.py +0 -0
- {nodeocc-1.0.11 → nodeocc-1.0.13}/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.13"
|
|
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"
|
|
@@ -70,7 +70,6 @@ JOB_LIMIT_PER_MSG = 60
|
|
|
70
70
|
parser = argparse.ArgumentParser(description='Visualize slurm jobs')
|
|
71
71
|
parser.add_argument('--debug', action='store_true', help='Enable logging')
|
|
72
72
|
parser.add_argument('--master_only', action='store_true', help='Disable all prints - only run in background')
|
|
73
|
-
parser.add_argument('--force_override', action='store_true', help='Force override of port file, kill previous instance')
|
|
74
73
|
parser.add_argument('--basepath', type=str, default='/nas/softechict-nas-2/mboschini/cool_scripts/new_nodeocc/', help='Base path for nodeocc. Must be readable by all users')
|
|
75
74
|
args = parser.parse_args()
|
|
76
75
|
|
|
@@ -123,7 +122,6 @@ def send_msg(instance: Singleton, msg, n_extra_msgs=0, is_extra=False):
|
|
|
123
122
|
def update_data_master(instance: Singleton):
|
|
124
123
|
if instance.check_existing_master_running():
|
|
125
124
|
instance.log(f"Master already running, killing current instance")
|
|
126
|
-
instance.cleanup()
|
|
127
125
|
exit(0)
|
|
128
126
|
|
|
129
127
|
inf = readers.slurmreader.read_infrastructure()
|
|
@@ -271,16 +269,6 @@ def _main():
|
|
|
271
269
|
assert orig_instance.is_master, "Instance is not `master`, run with `force_override` to override"
|
|
272
270
|
|
|
273
271
|
orig_instance.log(f"Starting master daemon")
|
|
274
|
-
# register atexit
|
|
275
|
-
import atexit
|
|
276
|
-
|
|
277
|
-
def exit_handler():
|
|
278
|
-
orig_instance.log(f"Exiting...")
|
|
279
|
-
orig_instance.sock.close()
|
|
280
|
-
# remove .port file
|
|
281
|
-
if orig_instance.port_file_exists():
|
|
282
|
-
Path(orig_instance.get_port_file_name()[1]).unlink()
|
|
283
|
-
atexit.register(exit_handler)
|
|
284
272
|
|
|
285
273
|
while True:
|
|
286
274
|
orig_instance.timeme(f"Updating...")
|
|
@@ -306,5 +294,4 @@ def _main():
|
|
|
306
294
|
|
|
307
295
|
_main()
|
|
308
296
|
|
|
309
|
-
orig_instance.cleanup()
|
|
310
297
|
exit(0)
|
|
@@ -13,6 +13,7 @@ import traceback
|
|
|
13
13
|
from logging.handlers import RotatingFileHandler
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from view import update_views
|
|
16
|
+
import stat
|
|
16
17
|
|
|
17
18
|
a_filter_values = [None, 'me', 'prod', 'stud', 'cvcs']
|
|
18
19
|
|
|
@@ -35,11 +36,21 @@ BUTTON_ACTIONS = {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
def is_file_writable_byall(file_path):
|
|
40
|
+
return os.stat(file_path).st_mode & stat.S_IWOTH
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_file_readable_byall(file_path):
|
|
44
|
+
return os.stat(file_path).st_mode & stat.S_IROTH
|
|
45
|
+
|
|
46
|
+
|
|
38
47
|
def try_open_socket_as_slave(instance):
|
|
39
|
-
if
|
|
48
|
+
if len(instance.get_port_files()) == 0:
|
|
40
49
|
raise Exception("No master running")
|
|
41
50
|
|
|
42
|
-
cur_port = int(
|
|
51
|
+
cur_port = [int(f.split('.')[0].split('master_')[1]) for f in instance.get_port_files()
|
|
52
|
+
if (pid := Path(f).read_text()) and psutil.pid_exists(int(pid))][0]
|
|
53
|
+
|
|
43
54
|
if hasattr(instance, 'port'):
|
|
44
55
|
# check if port file has same name
|
|
45
56
|
if instance.port != cur_port:
|
|
@@ -67,14 +78,6 @@ class Singleton:
|
|
|
67
78
|
Singleton.__instance.args = args
|
|
68
79
|
return Singleton.__instance
|
|
69
80
|
|
|
70
|
-
# destructor
|
|
71
|
-
def cleanup(self):
|
|
72
|
-
if self.is_master:
|
|
73
|
-
if self.port_file_exists():
|
|
74
|
-
os.system(f"rm {self.get_port_file_name()[1]}")
|
|
75
|
-
if hasattr(self, 'sock') and self.sock is not None:
|
|
76
|
-
self.sock.close()
|
|
77
|
-
|
|
78
81
|
def setup_logging(self):
|
|
79
82
|
if self.args.master_only:
|
|
80
83
|
handler = RotatingFileHandler(os.path.join(self.basepath, '.master_log.txt'), maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
|
|
@@ -88,9 +91,18 @@ class Singleton:
|
|
|
88
91
|
handlers=[handler])
|
|
89
92
|
|
|
90
93
|
def clean_port_files(self):
|
|
94
|
+
nodename = socket.getfqdn().split('.')[0]
|
|
91
95
|
for f in os.listdir(self.basepath):
|
|
92
|
-
if f.endswith('.
|
|
96
|
+
if nodename in f and 'master_' in f and f.endswith('.portfile') and is_file_writable_byall(os.path.join(self.basepath, f)):
|
|
97
|
+
# read pid from file
|
|
98
|
+
pid = Path(os.path.join(self.basepath, f)).read_text()
|
|
99
|
+
# assert no process is running
|
|
100
|
+
if psutil.pid_exists(int(pid)):
|
|
101
|
+
self.log(f"Process {pid} is still running, killing self")
|
|
102
|
+
return False
|
|
103
|
+
|
|
93
104
|
os.system(f"rm {os.path.join(self.basepath, f)}")
|
|
105
|
+
return True
|
|
94
106
|
|
|
95
107
|
def __init__(self, args=None):
|
|
96
108
|
""" Virtually private constructor. """
|
|
@@ -131,49 +143,36 @@ class Singleton:
|
|
|
131
143
|
|
|
132
144
|
self.basepath = self.args.basepath
|
|
133
145
|
|
|
134
|
-
# check if any .port file exists
|
|
135
|
-
if args.force_override and self.port_file_exists():
|
|
136
|
-
_, port_filepath = self.get_port_file_name()
|
|
137
|
-
|
|
138
|
-
# read pid from file
|
|
139
|
-
pid = Path(port_filepath).read_text()
|
|
140
|
-
# kill process
|
|
141
|
-
try:
|
|
142
|
-
os.kill(int(pid), signal.SIGTERM)
|
|
143
|
-
except Exception as e:
|
|
144
|
-
self.log("Could not kill process, deleting port file")
|
|
145
|
-
self.log(e)
|
|
146
|
-
|
|
147
|
-
# delete port file
|
|
148
|
-
Path(port_filepath).unlink()
|
|
149
|
-
|
|
150
146
|
self.is_master = not self.check_existing_master_running()
|
|
151
147
|
|
|
152
148
|
if self.is_master:
|
|
153
|
-
# clean up old port files
|
|
154
|
-
self.clean_port_files()
|
|
155
149
|
self.port = self.create_socket_as_master()
|
|
156
150
|
else:
|
|
157
151
|
try_open_socket_as_slave(self)
|
|
158
152
|
|
|
159
|
-
def get_port_file_name(self):
|
|
160
|
-
if self.port_file_exists():
|
|
161
|
-
fname = [f for f in os.listdir(self.basepath) if f.endswith('.port')][0]
|
|
162
|
-
return fname, os.path.join(self.basepath, fname)
|
|
163
|
-
return None
|
|
164
|
-
|
|
165
153
|
def check_existing_master_running(self):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
154
|
+
portfiles = self.get_port_files()
|
|
155
|
+
if len(portfiles) > 0:
|
|
156
|
+
for f in portfiles:
|
|
157
|
+
# check if process is still running
|
|
158
|
+
pid = Path(f).read_text()
|
|
159
|
+
if pid and int(pid) != os.getpid():
|
|
160
|
+
return psutil.pid_exists(int(pid))
|
|
171
161
|
return False
|
|
172
162
|
|
|
173
|
-
def
|
|
174
|
-
|
|
163
|
+
def get_port_files(self):
|
|
164
|
+
"""
|
|
165
|
+
Check if a port file exists in the basepath and the file has 666 permissions
|
|
166
|
+
"""
|
|
167
|
+
nodename = socket.getfqdn().split('.')[0]
|
|
168
|
+
portfiles = [os.path.join(self.basepath, f) for f in os.listdir(self.basepath)
|
|
169
|
+
if f.endswith('.portfile') and 'master_' in f and nodename in f]
|
|
170
|
+
portfiles = [f for f in portfiles if is_file_readable_byall(f) and is_file_writable_byall(f)]
|
|
171
|
+
return portfiles
|
|
175
172
|
|
|
176
173
|
def create_socket_as_master(self):
|
|
174
|
+
nodename = socket.getfqdn().split('.')[0]
|
|
175
|
+
|
|
177
176
|
# create udp socket for broadcasting
|
|
178
177
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
179
178
|
|
|
@@ -191,8 +190,26 @@ class Singleton:
|
|
|
191
190
|
# get pid of current process
|
|
192
191
|
self.pid = os.getpid()
|
|
193
192
|
|
|
194
|
-
#
|
|
195
|
-
|
|
193
|
+
# clean up old port files
|
|
194
|
+
kill_ok = self.clean_port_files()
|
|
195
|
+
if not kill_ok:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# create file to store port with 666 permissions to file
|
|
200
|
+
filepath = Path(self.basepath, f'{nodename}_master_{self.port}.portfile')
|
|
201
|
+
filepath.write_text(str(self.pid))
|
|
202
|
+
os.chmod(filepath, 0o666)
|
|
203
|
+
|
|
204
|
+
self.port_filepath = filepath
|
|
205
|
+
except Exception as e:
|
|
206
|
+
self.err(f"Could not create port file: {e}")
|
|
207
|
+
self.err(traceback.format_exc())
|
|
208
|
+
# check if file exists
|
|
209
|
+
if filepath.exists():
|
|
210
|
+
# delete file
|
|
211
|
+
os.system(f"rm {filepath}")
|
|
212
|
+
return None
|
|
196
213
|
|
|
197
214
|
return self.port
|
|
198
215
|
|
|
@@ -548,9 +565,12 @@ async def update_screen_info(stdscr, instance: Singleton):
|
|
|
548
565
|
if not instance.check_existing_master_running():
|
|
549
566
|
instance.log("MASTER STILL DEAD, TRYING TO BECOME MASTER")
|
|
550
567
|
instance.setup_logging()
|
|
551
|
-
instance.clean_port_files()
|
|
552
568
|
instance.port = instance.create_socket_as_master()
|
|
553
569
|
instance.is_master = True
|
|
570
|
+
else:
|
|
571
|
+
try_open_socket_as_slave(instance)
|
|
572
|
+
else:
|
|
573
|
+
try_open_socket_as_slave(instance)
|
|
554
574
|
|
|
555
575
|
update_screen(stdscr, instance)
|
|
556
576
|
|
|
@@ -626,7 +646,13 @@ async def main(stdscr):
|
|
|
626
646
|
|
|
627
647
|
def exit_handler(sig, frame):
|
|
628
648
|
instance.log(f"FORCED EXIT...")
|
|
629
|
-
instance.cleanup()
|
|
649
|
+
# instance.cleanup()
|
|
650
|
+
if hasattr(instance, 'port_filepath'):
|
|
651
|
+
os.system(f"rm {instance.port_filepath}")
|
|
652
|
+
|
|
653
|
+
if hasattr(instance, 'sock'):
|
|
654
|
+
instance.sock.close()
|
|
655
|
+
|
|
630
656
|
exit(0)
|
|
631
657
|
signal.signal(signal.SIGINT, exit_handler)
|
|
632
658
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|