foldspace 0.0.1__py3-none-any.whl
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.
- foldspace/args.py +391 -0
- foldspace/default_clusters.yml.j2 +239 -0
- foldspace/jobs.py +228 -0
- foldspace/main.py +204 -0
- foldspace/navigators.py +479 -0
- foldspace/prompts.py +108 -0
- foldspace/utils.py +277 -0
- foldspace-0.0.1.dist-info/METADATA +778 -0
- foldspace-0.0.1.dist-info/RECORD +13 -0
- foldspace-0.0.1.dist-info/WHEEL +5 -0
- foldspace-0.0.1.dist-info/entry_points.txt +2 -0
- foldspace-0.0.1.dist-info/licenses/LICENSE +674 -0
- foldspace-0.0.1.dist-info/top_level.txt +1 -0
foldspace/args.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Manage command line options for Foldspace package
|
|
3
|
+
# Authors: Fabio Pitari, Lucia Rodriguez Muñoz, Antonio Memmolo
|
|
4
|
+
|
|
5
|
+
import argparse, logging, sys, socket
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
if sys.version_info < (3, 10):
|
|
8
|
+
import importlib_resources as resources
|
|
9
|
+
else:
|
|
10
|
+
import importlib.resources as resources
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from rich.logging import RichHandler
|
|
14
|
+
from .utils import render_j2, rprint
|
|
15
|
+
from .jobs import list_queued_jobs
|
|
16
|
+
from .prompts import ask_if_resume, ask_resume_id
|
|
17
|
+
#from importlib import metadata # if retrieving version is needed
|
|
18
|
+
|
|
19
|
+
def get_arguments():
|
|
20
|
+
"""Retrieve variable from option passed at execution time. All the
|
|
21
|
+
unrecognized options are stored to be passed to the scheduler submit
|
|
22
|
+
command.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
args : object
|
|
27
|
+
ArgParse object containing native Foldspace options
|
|
28
|
+
batch_options : list
|
|
29
|
+
List containing all the unrecognized options (and values), to be
|
|
30
|
+
passed to submit command
|
|
31
|
+
"""
|
|
32
|
+
# N.B. do not start using logging from this function since this would
|
|
33
|
+
# force default logging settings, hiding all the other loggings. You can
|
|
34
|
+
# use it starting from check_local() function
|
|
35
|
+
|
|
36
|
+
parser = argparse.ArgumentParser(add_help=False, description="Foldspace provides access to HTTP services ('tools') on HPC clusters via queued jobs and SSH tunnels. The user supplies cluster connection details (see the --user, --host, and/or --cluster options) and selects a tool available in a cluster directory ('toolbox') using the --tool option. A job is submitted to run the chosen tool and returns the information needed to connect. Default resource values are set for these job submissions, but users will likely want to change them; sbatch/qsub options can therefore be appended to the foldspace command to tune the job. The tool implementation can either be in a default path when --cluster is used, or provided from a custom toolbox by specifying the --toolbox option (mandatory when using --host without --cluster). Foldspace is developed and mantained by Cineca HPC department, please check https://gitlab.hpc.cineca.it/interactive_computing/foldspace for more detailed documentation.")
|
|
37
|
+
|
|
38
|
+
parser.add_argument('--help', action='help', # not automated to avoid -h which conflicts with qsub -h
|
|
39
|
+
help='Print this help and exit')
|
|
40
|
+
|
|
41
|
+
#parser.add_argument('--version', action='version', version=f'%(prog)s {metadata.version("foldspace")}', # retrieve version from package
|
|
42
|
+
parser.add_argument('--version', action='version', version='', # relies on version printed in the logo
|
|
43
|
+
help='Print Foldspace version')
|
|
44
|
+
|
|
45
|
+
parser.add_argument('--user', metavar='USERNAME',
|
|
46
|
+
dest='user', required=False,
|
|
47
|
+
help='Username to use on the HPC cluster')
|
|
48
|
+
|
|
49
|
+
parser.add_argument('--host', metavar='HOST',
|
|
50
|
+
dest='host', required=False, default=None,
|
|
51
|
+
help='Remote ssh host to connect with. You can either use any host configured in your local SSH config file or the login node url coupling it with --user.')
|
|
52
|
+
|
|
53
|
+
parser.add_argument('--toolbox', metavar='REMOTE_PATH',
|
|
54
|
+
dest='toolbox', required=False, default=None,
|
|
55
|
+
help='Remote path on the cluster to which foldspace client will interact. A default value is automatically set when using --cluster option. If you want to deploy your own toolbox on an HPC cluster please read the Foldspace repo documentation')
|
|
56
|
+
|
|
57
|
+
parser.add_argument('--tool', metavar='TOOL',
|
|
58
|
+
dest='tool', required=False, default=None,
|
|
59
|
+
help='Tool to start (use --list-tools to list the available values in the toolbox)')
|
|
60
|
+
|
|
61
|
+
parser.add_argument('--list-tools', action='store_true',
|
|
62
|
+
dest='list_tools', required=False, default=False,
|
|
63
|
+
help='List avaliable tools in the toolbox of the given cluster/host for the --tool option')
|
|
64
|
+
|
|
65
|
+
parser.add_argument('--navigator', metavar="SSH_NAVIGATOR",
|
|
66
|
+
dest='navigator', required=False, default=None, # real default set in check_local()
|
|
67
|
+
help='Choose ssh implementation. Supported profiles are: socket (ssh commands from your system shell, using ssh sockets), nosocket (ssh commands, without sockets), paramiko (experimental, using paramiko and derivative packages). Default is "socket" (but if you are using the --cluster option default might differ for different clusters)')
|
|
68
|
+
|
|
69
|
+
parser.add_argument('--cluster', metavar='DEFAULT_CLUSTER',
|
|
70
|
+
dest='cluster', required=False, default=None,
|
|
71
|
+
help='Alias for predefined set of options for specific clusters. It requires --user and/or --host. You can use --list-clusters for the possible values. Specific HPC clusters might require some preliminary operation from your side to connect via ssh (e.g. retrieving certificates, setting known hosts) which might be executed by default by Foldspace; you can disable this behaviour via --no-startup-script option')
|
|
72
|
+
|
|
73
|
+
parser.add_argument('--list-clusters', action='store_true',
|
|
74
|
+
dest='list_clusters', required=False, default=False,
|
|
75
|
+
help='List avaliable default clusters for the --cluster option')
|
|
76
|
+
|
|
77
|
+
parser.add_argument('--no-startup-script', action='store_false',
|
|
78
|
+
dest='exec_startup_script', required=False, default=True,
|
|
79
|
+
help='If coupled with --cluster, avoids to run the startup script associated with it (you can inspect the scripts using the --list-clusters option)')
|
|
80
|
+
|
|
81
|
+
parser.add_argument('--port', metavar='LOCAL_PORT',
|
|
82
|
+
dest='port', required=False, default=None, type=int, # real default set in check_local()
|
|
83
|
+
help='Local port to be used to expose and access the tool from your browser. Default is a random free port')
|
|
84
|
+
|
|
85
|
+
parser.add_argument('--keyfile', metavar="SSH_KEYFILE",
|
|
86
|
+
dest='keyfile', required=False, default=None,
|
|
87
|
+
help='Path for ssh private key to be used in the ssh connections')
|
|
88
|
+
|
|
89
|
+
parser.add_argument('--debug', action='store_true',
|
|
90
|
+
dest='debug', required=False, default=False,
|
|
91
|
+
help='Run in debug mode')
|
|
92
|
+
|
|
93
|
+
parser.add_argument("-/--<any batch option>", metavar="VALUE", nargs="*", default=[],
|
|
94
|
+
help="You can add any option of sbatch/qsub command to overwrite/complement default values of the tool's job. Check 'man sbatch'/'man qsub' on your cluster for details.")
|
|
95
|
+
|
|
96
|
+
# All the values from known options are stored as attributes into
|
|
97
|
+
# args; all the unknown options, values and arguments are stored as
|
|
98
|
+
# strings into the batch_options list. N.B. this means that, in case
|
|
99
|
+
# of namesake options, foldspace ones prevails, thus keep them as
|
|
100
|
+
# unambiguous as possible
|
|
101
|
+
args, batch_options = parser.parse_known_args()
|
|
102
|
+
|
|
103
|
+
return args, batch_options
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_local(args, batch_args=[]):
|
|
107
|
+
"""Elaborate all the options from get_arguments() to translate into
|
|
108
|
+
variables. It might also quit the code if the options are not enough
|
|
109
|
+
or inconsistent, or if they're simple enough not to require any
|
|
110
|
+
action from the main code (e.g. --list-clusters)
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
args : object
|
|
115
|
+
ArgParse object containing parsed options
|
|
116
|
+
batch_args : list, optional
|
|
117
|
+
List of options and arguments to be passed to job scheduler at submit
|
|
118
|
+
time
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
host
|
|
123
|
+
SSH cluster host (either just the url or an host in ~/.ssh/config.
|
|
124
|
+
String.
|
|
125
|
+
keyfile
|
|
126
|
+
Identity file to be used in ssh commands. Either string containing
|
|
127
|
+
path or None.
|
|
128
|
+
local_port
|
|
129
|
+
Local port on which expose tools. Integer.
|
|
130
|
+
batch_options
|
|
131
|
+
User-defined for sbatch/qsub command in Slurm/PBS schedulers. String.
|
|
132
|
+
SshNavigator
|
|
133
|
+
One of the classes from the 'navigators' module in Foldspace
|
|
134
|
+
startup_script_content
|
|
135
|
+
Python code to be run before SSH initialization for default
|
|
136
|
+
clusters (if defined). Either string or None
|
|
137
|
+
tool
|
|
138
|
+
Tool to run in the batch job. Either string or None
|
|
139
|
+
toolbox
|
|
140
|
+
Remote path containing the toolbox files. String.
|
|
141
|
+
list_tools
|
|
142
|
+
Defines if the user asked to list the toolbox tools or not. Boolean
|
|
143
|
+
user
|
|
144
|
+
SSH user. Either string or None (in case it is defined in
|
|
145
|
+
~/.ssh/config)
|
|
146
|
+
"""
|
|
147
|
+
# Enable debugging infos when requested
|
|
148
|
+
if args.debug == True:
|
|
149
|
+
logging.basicConfig(
|
|
150
|
+
format="{asctime} {message}",
|
|
151
|
+
style="{",
|
|
152
|
+
datefmt="%Y-%m-%d %H:%M",
|
|
153
|
+
level=logging.DEBUG,
|
|
154
|
+
handlers=[RichHandler()]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# N.B. you can use logging from now on
|
|
158
|
+
logging.info("Entering in check_local() function")
|
|
159
|
+
|
|
160
|
+
# Initialize variables
|
|
161
|
+
cluster, exec_startup_script, host, keyfile, list_tools, local_port, navigator, tool, toolbox, user = args.cluster, args.exec_startup_script, args.host, args.keyfile, args.list_tools, args.port, args.navigator, args.tool, args.toolbox, args.user
|
|
162
|
+
batch_options = " ".join(batch_args)
|
|
163
|
+
startup_script_content = None
|
|
164
|
+
|
|
165
|
+
# Debug
|
|
166
|
+
logging.info("Foldspace options/arguments parsed from options: {raw_options}".format(raw_options={ k:v for k,v in vars(args).items() if '<any batch option>' not in k}))
|
|
167
|
+
logging.info(f"Scheduler options/arguments parsed from options: {batch_options.strip() or 'none'}")
|
|
168
|
+
|
|
169
|
+
# In case --list-clusters option was used, print default clusters
|
|
170
|
+
# list and exit
|
|
171
|
+
if args.list_clusters:
|
|
172
|
+
with resources.files(__package__).joinpath("default_clusters.yml.j2").open("r") as d:
|
|
173
|
+
clusters = yaml.safe_load(d) # yaml to dictionary, not rendered
|
|
174
|
+
rprint("Available clusters for the --cluster option:", text_style="cyan", prepend="╰─┬─ ")
|
|
175
|
+
for cluster in clusters.keys():
|
|
176
|
+
if cluster != list(clusters.keys())[-1]:
|
|
177
|
+
rprint(cluster, prepend=" ├─── ")
|
|
178
|
+
else:
|
|
179
|
+
rprint(cluster, prepend=" ╰─── ")
|
|
180
|
+
for cluster in clusters.keys():
|
|
181
|
+
rprint(f"{clusters[cluster]['description'].strip()}", title=cluster, border_style="cyan")
|
|
182
|
+
if "startup_script" in clusters[cluster].keys():
|
|
183
|
+
rprint(f"Using --cluster={cluster} the following script will be automatically run on your computer before connecting to {cluster} via ssh (unless you add the --no-startup-script option):\n\n{clusters[cluster]['startup_script'].strip()}", title=f"Startup script for {cluster}", border_style="yellow")
|
|
184
|
+
sys.exit(0)
|
|
185
|
+
|
|
186
|
+
### In any other case you need one between --host and --cluster
|
|
187
|
+
### If neither --cluster and --host are specified
|
|
188
|
+
if cluster == None and host ==None:
|
|
189
|
+
raise InvalidOptions("Missing minimal options (--list-clusters, --cluster or --host), please check --help")
|
|
190
|
+
|
|
191
|
+
# In any other case you specified one of the two.
|
|
192
|
+
# If --cluster is specified (with or without --host)
|
|
193
|
+
elif cluster != None:
|
|
194
|
+
# User has to be either defined in ssh host (passed via --host)
|
|
195
|
+
# or passed via --user, thus exit if both are missing
|
|
196
|
+
if user == None and host == None:
|
|
197
|
+
raise InvalidOptions("The --cluster option requires the --user option and/or the --host option.")
|
|
198
|
+
|
|
199
|
+
# load yaml file containing default clusters
|
|
200
|
+
with resources.files(__package__).joinpath("default_clusters.yml.j2").open("r") as d:
|
|
201
|
+
default_clusters = yaml.safe_load(render_j2(d.read(), **locals())) # yaml to dictionary
|
|
202
|
+
|
|
203
|
+
# If cluster is known, set variables accordingly
|
|
204
|
+
if cluster in default_clusters:
|
|
205
|
+
# Use default host if not differently specified via
|
|
206
|
+
# --host option
|
|
207
|
+
if not host:
|
|
208
|
+
host = default_clusters[cluster]["host"].strip()
|
|
209
|
+
logging.info(f"Default host {host} for {cluster} will be used")
|
|
210
|
+
# Use default toolbox if not differently specified via
|
|
211
|
+
# --toolbox option
|
|
212
|
+
if not toolbox:
|
|
213
|
+
toolbox = default_clusters[cluster]["toolbox"].strip()
|
|
214
|
+
logging.info(f"Default toolbox {toolbox} for {cluster} will be used")
|
|
215
|
+
# Use default navigator if specified as cluster defaults and
|
|
216
|
+
# not differently specified via --navigator option
|
|
217
|
+
if not navigator and "navigator" in default_clusters[cluster].keys():
|
|
218
|
+
navigator = default_clusters[cluster]["navigator"].strip()
|
|
219
|
+
logging.info(f"Default navigator {navigator} for {cluster} will be used")
|
|
220
|
+
# Dump startup script content if present in cluster defaults
|
|
221
|
+
# and --no-startup-script is not specified
|
|
222
|
+
if exec_startup_script and "startup_script" in default_clusters[cluster].keys():
|
|
223
|
+
startup_script_content = default_clusters[cluster]["startup_script"].strip()
|
|
224
|
+
logging.info(f"Startup script for {cluster} will be run")
|
|
225
|
+
# If the cluster is unknown, exit
|
|
226
|
+
else:
|
|
227
|
+
raise InvalidOptions(f"Cluster {cluster} is unknown, check --list-clusters for availiable default clusters or specify both --host and --toolbox for custom clusters.")
|
|
228
|
+
|
|
229
|
+
# If just --host is specified
|
|
230
|
+
elif host != None:
|
|
231
|
+
# You can't go on if the toolbox of a custom cluster is
|
|
232
|
+
# not specified (all the variables remain unchanged; --user
|
|
233
|
+
# is not enforced since user might be already specified in a
|
|
234
|
+
# ~/.ssh/config host)
|
|
235
|
+
if toolbox is None:
|
|
236
|
+
raise InvalidOptions(f"You need to specify --toolbox too for the specific toolbox path on {host} host")
|
|
237
|
+
|
|
238
|
+
# Set random (free) local port if not specified by user
|
|
239
|
+
if not local_port:
|
|
240
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
241
|
+
s.bind(("localhost", 0)) # 0 is a random free port
|
|
242
|
+
local_port = s.getsockname()[1]
|
|
243
|
+
logging.info(f"Local port {local_port} is free, chosen.")
|
|
244
|
+
|
|
245
|
+
# Set default value for navigator if not previously set via --cluster
|
|
246
|
+
# defaults or via --navigator option
|
|
247
|
+
if not navigator:
|
|
248
|
+
logging.info("Navigator not set so far, thus the default value 'socket' will be used")
|
|
249
|
+
navigator = "socket"
|
|
250
|
+
|
|
251
|
+
# Import navigator
|
|
252
|
+
if navigator == "nosocket":
|
|
253
|
+
from .navigators import NoSocketNavigator as SshNavigator
|
|
254
|
+
elif navigator == "socket":
|
|
255
|
+
from .navigators import SocketNavigator as SshNavigator
|
|
256
|
+
elif navigator == "paramiko":
|
|
257
|
+
from .navigators import ParamikoNavigator as SshNavigator
|
|
258
|
+
else:
|
|
259
|
+
raise InvalidOptions(f"Navigator {navigator} unknown, please check --help for supported values")
|
|
260
|
+
|
|
261
|
+
return batch_options, host, keyfile, list_tools, local_port, SshNavigator, startup_script_content, tool, toolbox, user
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_remote(list_tools, navigator, host, tool, toolbox):
|
|
265
|
+
"""Elaborate all the options from get_arguments() and check_local
|
|
266
|
+
with respect to the remote side on the cluster, i.e. the toolbox,
|
|
267
|
+
and the user specific permissions to see tools.
|
|
268
|
+
It might also quit the code if the options are not enough or
|
|
269
|
+
inconsistent, or if they're simple enough not to require any action
|
|
270
|
+
from the main code (e.g. --list-tools)
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
navigator : object
|
|
275
|
+
An object from one of the classes in the 'navigators' module
|
|
276
|
+
toolbox : str
|
|
277
|
+
The path of the toolbox on the cluster
|
|
278
|
+
tool : str or None
|
|
279
|
+
The tool requested (if requested, see 'list_tools')
|
|
280
|
+
list_tools : bool
|
|
281
|
+
Wether the user asked to just list the tools or not
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
dict
|
|
286
|
+
Data retrieved remotely from toolbox.yml
|
|
287
|
+
int or None
|
|
288
|
+
When restoring, it's the job id to resume
|
|
289
|
+
"""
|
|
290
|
+
logging.info("Entering in check_remote() function")
|
|
291
|
+
|
|
292
|
+
# Validate toolbox
|
|
293
|
+
run_toolbox = navigator.command(f"cat {toolbox}/toolbox.yml")
|
|
294
|
+
if run_toolbox['rc'] != 0:
|
|
295
|
+
rprint(f"Path {toolbox} on {host} doesn't seem to be a valid toolbox", text_style="red", prepend="╰──── ")
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
toolbox_data = yaml.safe_load(run_toolbox['out'])
|
|
299
|
+
|
|
300
|
+
# Get user tool access for the given toolbox
|
|
301
|
+
allowed_tools_cmd = f"""
|
|
302
|
+
for d in {toolbox}/tools/*; do
|
|
303
|
+
if [ -d "$d" ] && [ -r "$d" ] && [ -x "$d" ]; then
|
|
304
|
+
printf '%s\n' "${{d##*/}}"
|
|
305
|
+
fi
|
|
306
|
+
done
|
|
307
|
+
"""
|
|
308
|
+
allowed_tools_cmd_out = navigator.command(allowed_tools_cmd)
|
|
309
|
+
allowed_tools = allowed_tools_cmd_out["out"].split()
|
|
310
|
+
logging.debug(f"Tools enabled for user in the given toolbox: {allowed_tools}")
|
|
311
|
+
|
|
312
|
+
# Filter toolbox_data using user permissions
|
|
313
|
+
toolbox_data["tools"] = {
|
|
314
|
+
tool_name: tool_data
|
|
315
|
+
for tool_name, tool_data in toolbox_data.get("tools", {}).items()
|
|
316
|
+
if tool_name in allowed_tools
|
|
317
|
+
}
|
|
318
|
+
logging.debug(f"Data parsed from toolbox.yml: {toolbox_data}")
|
|
319
|
+
|
|
320
|
+
# In case --list-tools option was used, print tools list and exit
|
|
321
|
+
if list_tools:
|
|
322
|
+
rprint("Available tools for the --tool option:", text_style="cyan", prepend="╰─┬─ ")
|
|
323
|
+
for tool_name in toolbox_data['tools'].keys():
|
|
324
|
+
if tool_name != list(toolbox_data['tools'].keys())[-1]:
|
|
325
|
+
rprint(tool_name, prepend=" ├─── ")
|
|
326
|
+
else:
|
|
327
|
+
rprint(tool_name, prepend=" ╰─── ")
|
|
328
|
+
for tool_name, tool_description in toolbox_data['tools'].items():
|
|
329
|
+
rprint(tool_description, title=tool_name, border_style="cyan")
|
|
330
|
+
sys.exit(0)
|
|
331
|
+
|
|
332
|
+
# Retrieve the list of past jobs, and which one of them is active
|
|
333
|
+
resume_id, active_tools = None, {} # init
|
|
334
|
+
tools_history = yaml.safe_load(navigator.command(f"cat {toolbox_data['user_dir']}/tools_history.yml")['out']) # N.B. returns None if file doesn't exist
|
|
335
|
+
if tools_history:
|
|
336
|
+
active_jobs = list_queued_jobs(navigator, toolbox_data['scheduler'])
|
|
337
|
+
skipped_states = ["COMPLETING"]
|
|
338
|
+
active_tools = {job_id: state for job_id, state in active_jobs.items() if job_id in set(tools_history.keys()) and state not in skipped_states} # set() increase efficiency if the history is very long
|
|
339
|
+
|
|
340
|
+
# If there are no active tools and neither --list-tools nor --tool
|
|
341
|
+
# were specified, exit
|
|
342
|
+
if (not active_tools) and (not tool):
|
|
343
|
+
rprint(f"No tools currently queued on {toolbox_data['cluster_name']} and --tool option was not specified", text_style="red", prepend="╰─── ")
|
|
344
|
+
sys.exit(0)
|
|
345
|
+
|
|
346
|
+
# If the user survived until here, this means that either he/she is
|
|
347
|
+
# starting a new tool or restoring a tool which is still active.
|
|
348
|
+
# If user has tools already queued, check what to do
|
|
349
|
+
if active_tools:
|
|
350
|
+
|
|
351
|
+
# List active tools
|
|
352
|
+
rprint(f"You have tools already queued on the cluster", text_style="yellow", prepend = "├─┬─ ")
|
|
353
|
+
rprint("", text_style="cyan", prepend = "│ │ ")
|
|
354
|
+
|
|
355
|
+
for old_job_id, old_job_state in active_tools.items():
|
|
356
|
+
old_toolbox = tools_history[old_job_id]['toolbox']
|
|
357
|
+
old_tool = tools_history[old_job_id]['tool']
|
|
358
|
+
old_batch_options = tools_history[old_job_id]['batch_options']
|
|
359
|
+
old_submit_timestamp = datetime.fromisoformat(str(tools_history[old_job_id]['submit_timestamp'])).astimezone().strftime("%A, %B %d, %Y, %H:%M:%S %Z")
|
|
360
|
+
|
|
361
|
+
rprint(f"{old_tool} in job {old_job_id} (state: {old_job_state})", text_style="medium_purple", prepend="│ ├─┬─ ")
|
|
362
|
+
rprint(f"toolbox: [grey63]{old_toolbox}[/grey63]", prepend="│ │ ├─── ")
|
|
363
|
+
if old_batch_options:
|
|
364
|
+
rprint(f"batch options: [cyan]{old_batch_options}[/cyan]", prepend="│ │ ├─── ")
|
|
365
|
+
else:
|
|
366
|
+
rprint(f"batch options: [grey50]none[/grey50]", prepend="│ │ ├─── ")
|
|
367
|
+
rprint(f"submitted on: [green]{old_submit_timestamp}[/green]", prepend="│ │ ╰─── ")
|
|
368
|
+
rprint("", text_style="cyan", prepend = "│ │ ")
|
|
369
|
+
|
|
370
|
+
# Resume automatically when --tool was not passed; ask what to
|
|
371
|
+
# do if it was passed. If user decides not to resume tools,
|
|
372
|
+
# resume_id keeps staying to None
|
|
373
|
+
if not tool: # just for better graphic output
|
|
374
|
+
rprint(f"Your tool is going to be resumed", prepend="│ ╰─ ")
|
|
375
|
+
if (not tool) or (ask_if_resume() == True):
|
|
376
|
+
resume_id = ask_resume_id(tools_history, active_tools)
|
|
377
|
+
|
|
378
|
+
# If --tool was specified and there are no active jobs, user wants
|
|
379
|
+
# unequivocally submit a new tool job; in this case it will be
|
|
380
|
+
# checked if it's present in the toolbox
|
|
381
|
+
if tool and not resume_id:
|
|
382
|
+
if tool not in toolbox_data['tools'].keys():
|
|
383
|
+
rprint(f'Tool {tool} is not available in the toolbox. Available tools: {", ".join(list(toolbox_data["tools"].keys()))}. Please use the --list-tools option if you need details of these tools', text_style="red", prepend="╰──── ")
|
|
384
|
+
sys.exit(1)
|
|
385
|
+
|
|
386
|
+
return toolbox_data, resume_id
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class InvalidOptions(Exception):
|
|
390
|
+
"Placeholder to define a specific exception for unusable options"
|
|
391
|
+
pass
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
leonardo:
|
|
2
|
+
host: login.leonardo.cineca.it
|
|
3
|
+
toolbox: "/leonardo/prod/opt/tools/foldspace/1.0/"
|
|
4
|
+
description: |
|
|
5
|
+
Leonardo is the current Tier-0 system at Cineca. Please take a look to https://www.hpc.cineca.it/systems/hardware/leonardo/ for details.
|
|
6
|
+
navigator: nosocket
|
|
7
|
+
startup_script: |
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
#SMALLSTEP_IDENTITY = "{{ user if user else host }}@foldspace"
|
|
15
|
+
SMALLSTEP_IDENTITY = "foldspace"
|
|
16
|
+
|
|
17
|
+
LEONARDO_ADDRESSES = [
|
|
18
|
+
"login01-ext.leonardo.cineca.it",
|
|
19
|
+
"login02-ext.leonardo.cineca.it",
|
|
20
|
+
"login05-ext.leonardo.cineca.it",
|
|
21
|
+
"login07-ext.leonardo.cineca.it",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
KEY_ALGORITHMS = ["rsa", "ecdsa", "ed25519", "dsa"]
|
|
25
|
+
|
|
26
|
+
WILDCARD_HOST = "login*.leonardo.cineca.it"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Stop execution if smallstep binary is not present in the PATH environment variable
|
|
30
|
+
if shutil.which("step") is None:
|
|
31
|
+
print(
|
|
32
|
+
"Error: no Smallstep binary found (step command)",
|
|
33
|
+
file=sys.stderr,
|
|
34
|
+
)
|
|
35
|
+
sys.exit(127)
|
|
36
|
+
|
|
37
|
+
# Retrieve smallstep certificate if not already present
|
|
38
|
+
list_proc = subprocess.run(
|
|
39
|
+
["step", "ssh", "list", "--raw", SMALLSTEP_IDENTITY],
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
inspect_proc = subprocess.run(
|
|
44
|
+
["step", "ssh", "inspect"],
|
|
45
|
+
input=list_proc.stdout,
|
|
46
|
+
stdout=subprocess.PIPE,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
if "Valid" not in inspect_proc.stdout:
|
|
50
|
+
print("Running smallstep client to allow ssh connection toward Cineca clusters...")
|
|
51
|
+
subprocess.run(
|
|
52
|
+
["step", "ssh", "login", SMALLSTEP_IDENTITY, "--provisioner", "cineca-hpc"]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Set ssh known hosts properly for leonardo login nodes
|
|
56
|
+
ssh_dir = Path.home() / ".ssh"
|
|
57
|
+
previous_umask = os.umask(0o077)
|
|
58
|
+
try:
|
|
59
|
+
ssh_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
finally:
|
|
61
|
+
os.umask(previous_umask)
|
|
62
|
+
|
|
63
|
+
known_hosts = ssh_dir / "known_hosts"
|
|
64
|
+
|
|
65
|
+
# remove previous values for leonardo
|
|
66
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", "login.leonardo.cineca.it"])
|
|
67
|
+
for address in LEONARDO_ADDRESSES:
|
|
68
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", address])
|
|
69
|
+
|
|
70
|
+
with open(known_hosts, "a") as known_hosts_file:
|
|
71
|
+
for keyalg in KEY_ALGORITHMS:
|
|
72
|
+
for address in LEONARDO_ADDRESSES:
|
|
73
|
+
scan_proc = subprocess.run(
|
|
74
|
+
["ssh-keyscan", "-t", keyalg, address],
|
|
75
|
+
stdout=subprocess.PIPE,
|
|
76
|
+
text=True,
|
|
77
|
+
)
|
|
78
|
+
known_hosts_file.write(scan_proc.stdout.replace(address, WILDCARD_HOST))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
g100:
|
|
82
|
+
host: login.g100.cineca.it
|
|
83
|
+
toolbox: "/cineca/prod/opt/tools/foldspace/1.0/"
|
|
84
|
+
description: |
|
|
85
|
+
Galileo 100 is the current Tier-1 system at Cineca. Please take a look to https://www.hpc.cineca.it/systems/hardware/galileo100/ for details.
|
|
86
|
+
navigator: nosocket
|
|
87
|
+
startup_script: |
|
|
88
|
+
import os
|
|
89
|
+
import shutil
|
|
90
|
+
import subprocess
|
|
91
|
+
import sys
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
|
|
94
|
+
#SMALLSTEP_IDENTITY = "{{ user if user else host }}@foldspace"
|
|
95
|
+
SMALLSTEP_IDENTITY = "foldspace"
|
|
96
|
+
|
|
97
|
+
G100_ADDRESSES = [
|
|
98
|
+
"login01-ext.g100.cineca.it",
|
|
99
|
+
"login02-ext.g100.cineca.it",
|
|
100
|
+
"login03-ext.g100.cineca.it",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
KEY_ALGORITHMS = ["rsa", "ecdsa", "ed25519", "dsa"]
|
|
104
|
+
|
|
105
|
+
WILDCARD_HOST = "login*.g100.cineca.it"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Stop execution if smallstep binary is not present in the PATH environment variable
|
|
109
|
+
if shutil.which("step") is None:
|
|
110
|
+
print(
|
|
111
|
+
"Error: no Smallstep binary found (step command)",
|
|
112
|
+
file=sys.stderr,
|
|
113
|
+
)
|
|
114
|
+
sys.exit(127)
|
|
115
|
+
|
|
116
|
+
# Retrieve smallstep certificate if not already present
|
|
117
|
+
list_proc = subprocess.run(
|
|
118
|
+
["step", "ssh", "list", "--raw", SMALLSTEP_IDENTITY],
|
|
119
|
+
stdout=subprocess.PIPE,
|
|
120
|
+
text=True,
|
|
121
|
+
)
|
|
122
|
+
inspect_proc = subprocess.run(
|
|
123
|
+
["step", "ssh", "inspect"],
|
|
124
|
+
input=list_proc.stdout,
|
|
125
|
+
stdout=subprocess.PIPE,
|
|
126
|
+
text=True,
|
|
127
|
+
)
|
|
128
|
+
if "Valid" not in inspect_proc.stdout:
|
|
129
|
+
print("Running smallstep client to allow ssh connection toward Cineca clusters...")
|
|
130
|
+
subprocess.run(
|
|
131
|
+
["step", "ssh", "login", SMALLSTEP_IDENTITY, "--provisioner", "cineca-hpc"]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Set ssh known hosts properly for g100 login nodes
|
|
135
|
+
ssh_dir = Path.home() / ".ssh"
|
|
136
|
+
previous_umask = os.umask(0o077)
|
|
137
|
+
try:
|
|
138
|
+
ssh_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
finally:
|
|
140
|
+
os.umask(previous_umask)
|
|
141
|
+
|
|
142
|
+
known_hosts = ssh_dir / "known_hosts"
|
|
143
|
+
|
|
144
|
+
# remove previous values for g100
|
|
145
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", "login.g100.cineca.it"])
|
|
146
|
+
for address in G100_ADDRESSES:
|
|
147
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", address])
|
|
148
|
+
|
|
149
|
+
with open(known_hosts, "a") as known_hosts_file:
|
|
150
|
+
for keyalg in KEY_ALGORITHMS:
|
|
151
|
+
for address in G100_ADDRESSES:
|
|
152
|
+
scan_proc = subprocess.run(
|
|
153
|
+
["ssh-keyscan", "-t", keyalg, address],
|
|
154
|
+
stdout=subprocess.PIPE,
|
|
155
|
+
text=True,
|
|
156
|
+
)
|
|
157
|
+
known_hosts_file.write(scan_proc.stdout.replace(address, WILDCARD_HOST))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
pitagora:
|
|
161
|
+
host: login.pitagora.cineca.it
|
|
162
|
+
toolbox: "/pitagora/prod/opt/tools/foldspace/1.0/"
|
|
163
|
+
description: |
|
|
164
|
+
Pitagora is the new Tier-1 system hosted by CINECA and fully dedicated to the EUROFusion community. Please take a look to https://docs.hpc.cineca.it/hpc/pitagora.html for details.
|
|
165
|
+
navigator: nosocket
|
|
166
|
+
startup_script: |
|
|
167
|
+
import os
|
|
168
|
+
import shutil
|
|
169
|
+
import subprocess
|
|
170
|
+
import sys
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
#SMALLSTEP_IDENTITY = "{{ user if user else host }}@foldspace"
|
|
174
|
+
SMALLSTEP_IDENTITY = "foldspace"
|
|
175
|
+
|
|
176
|
+
PITAGORA_ADDRESSES = [
|
|
177
|
+
"login01-ext.pitagora.cineca.it",
|
|
178
|
+
"login02-ext.pitagora.cineca.it",
|
|
179
|
+
"login03-ext.pitagora.cineca.it",
|
|
180
|
+
"login04-ext.pitagora.cineca.it",
|
|
181
|
+
"login05-ext.pitagora.cineca.it",
|
|
182
|
+
"login06-ext.pitagora.cineca.it",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
KEY_ALGORITHMS = ["rsa", "ecdsa", "ed25519", "dsa"]
|
|
186
|
+
|
|
187
|
+
WILDCARD_HOST = "login*.pitagora.cineca.it"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Stop execution if smallstep binary is not present in the PATH environment variable
|
|
191
|
+
if shutil.which("step") is None:
|
|
192
|
+
print(
|
|
193
|
+
"Error: no Smallstep binary found (step command)",
|
|
194
|
+
file=sys.stderr,
|
|
195
|
+
)
|
|
196
|
+
sys.exit(127)
|
|
197
|
+
|
|
198
|
+
# Retrieve smallstep certificate if not already present
|
|
199
|
+
list_proc = subprocess.run(
|
|
200
|
+
["step", "ssh", "list", "--raw", SMALLSTEP_IDENTITY],
|
|
201
|
+
stdout=subprocess.PIPE,
|
|
202
|
+
text=True,
|
|
203
|
+
)
|
|
204
|
+
inspect_proc = subprocess.run(
|
|
205
|
+
["step", "ssh", "inspect"],
|
|
206
|
+
input=list_proc.stdout,
|
|
207
|
+
stdout=subprocess.PIPE,
|
|
208
|
+
text=True,
|
|
209
|
+
)
|
|
210
|
+
if "Valid" not in inspect_proc.stdout:
|
|
211
|
+
print("Running smallstep client to allow ssh connection toward Cineca clusters...")
|
|
212
|
+
subprocess.run(
|
|
213
|
+
["step", "ssh", "login", SMALLSTEP_IDENTITY, "--provisioner", "cineca-hpc"]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Set ssh known hosts properly for pitagora login nodes
|
|
217
|
+
ssh_dir = Path.home() / ".ssh"
|
|
218
|
+
previous_umask = os.umask(0o077)
|
|
219
|
+
try:
|
|
220
|
+
ssh_dir.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
finally:
|
|
222
|
+
os.umask(previous_umask)
|
|
223
|
+
|
|
224
|
+
known_hosts = ssh_dir / "known_hosts"
|
|
225
|
+
|
|
226
|
+
# remove previous values for pitagora
|
|
227
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", "login.pitagora.cineca.it"])
|
|
228
|
+
for address in PITAGORA_ADDRESSES:
|
|
229
|
+
subprocess.run(["ssh-keygen", "-f", str(known_hosts), "-R", address])
|
|
230
|
+
|
|
231
|
+
with open(known_hosts, "a") as known_hosts_file:
|
|
232
|
+
for keyalg in KEY_ALGORITHMS:
|
|
233
|
+
for address in PITAGORA_ADDRESSES:
|
|
234
|
+
scan_proc = subprocess.run(
|
|
235
|
+
["ssh-keyscan", "-t", keyalg, address],
|
|
236
|
+
stdout=subprocess.PIPE,
|
|
237
|
+
text=True,
|
|
238
|
+
)
|
|
239
|
+
known_hosts_file.write(scan_proc.stdout.replace(address, WILDCARD_HOST))
|