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 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))