Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2c2a76a
working on paramiko sftp transferer
chrisdjscott Jul 24, 2025
ebc278a
working on paramiko ssh runner
chrisdjscott Aug 8, 2025
ce47889
config update and paramiko runner
chrisdjscott Aug 15, 2025
53d423c
feat: add SSH keypair generation method to NeSISetup
chrisdjscott Aug 15, 2025
398d9ca
feat: add --ssh option to generate SSH key pair
chrisdjscott Aug 15, 2025
0fc3d90
feat: add --no-globus option and conditional Paramiko key override
chrisdjscott Aug 15, 2025
78764d4
feat: skip authentication step when --no-globus flag is used
chrisdjscott Aug 15, 2025
8a724c2
feat: add remote_user override for Paramiko when SSH enabled
chrisdjscott Aug 15, 2025
376d799
feat: add Paramiko setup prompting base path default /tmp/<user>/rjm
chrisdjscott Aug 15, 2025
6b67c66
feat: add get_paramiko_config and use it in rjm_config
chrisdjscott Aug 15, 2025
9d8b1b6
fix
chrisdjscott Aug 15, 2025
d44a584
paramiko setup
chrisdjscott Aug 15, 2025
72dcebd
feat: add public key prompt after SSH key generation and fix note typo
chrisdjscott Aug 15, 2025
16797c0
feat: add remote address prompt to Paramiko setup
chrisdjscott Aug 15, 2025
47e13d8
feat: add remote_address override for Paramiko in rjm_config
chrisdjscott Aug 15, 2025
90fb62b
feat: drop --no-globus, use --ssh to skip Globus setup
chrisdjscott Aug 22, 2025
6168d9d
refactor: drop --no-globus flag and enhance SSH help message
chrisdjscott Aug 22, 2025
a86023a
setting component types in rjm_config
chrisdjscott Aug 22, 2025
3afe366
different implementation of check dir exists
chrisdjscott Aug 22, 2025
eed1ada
style: revise placeholder comment in ParamikoSSHRunner
chrisdjscott Aug 22, 2025
aa71997
feat: implement remote directory existence check in ParamikoSSHRunner
chrisdjscott Aug 22, 2025
dfd1f06
docs: add comment on selecting runner via COMPONENTS config
chrisdjscott Aug 22, 2025
7aa38a3
feat: choose remote runner based on config COMPONENTS.runner setting
chrisdjscott Aug 22, 2025
83efc31
feat: select remote runner based on config COMPONENTS.runner
chrisdjscott Aug 22, 2025
bce2712
docs: add comment describing configurable transferer selection
chrisdjscott Aug 22, 2025
26baf6b
feat: select file transferer based on config setting
chrisdjscott Aug 22, 2025
4ce034e
feat: select runner and transferer from config in RemoteJobBatch
chrisdjscott Aug 22, 2025
bb4b7cd
feat: add ParamikoSSHRunner health check with run_command support
chrisdjscott Aug 22, 2025
02cbf0e
fix: correct string quoting in remote health check cleanup commands
chrisdjscott Aug 22, 2025
ccd7c57
runner and transferer setup on RemoteJob
chrisdjscott Aug 22, 2025
29117ba
chore: add note to switch SSH key loading to RSA
chrisdjscott Aug 22, 2025
a893887
fix: use RSAKey for SSH authentication instead of Ed25519Key
chrisdjscott Aug 22, 2025
93667f2
chore: add comment on attempting RSA or Ed25519 key loading
chrisdjscott Aug 22, 2025
ff80783
feat: add fallback to load Ed25519 SSH key
chrisdjscott Aug 22, 2025
ff0c114
working on ssh keys
chrisdjscott Aug 23, 2025
c409a48
docs: annotate key file comments with AI note
chrisdjscott Aug 23, 2025
96b05f6
feat: write private and public keys to files with correct permissions
chrisdjscott Aug 23, 2025
87c6d08
fix
chrisdjscott Aug 24, 2025
85cabb7
fix: use RSA key, correct remote path handling and listdir path
chrisdjscott Aug 25, 2025
54a4fff
fix: ensure remote base path exists by creating missing directory
chrisdjscott Aug 25, 2025
6b00d89
fixing paramiko runner/transfer and health check
chrisdjscott Aug 25, 2025
0660954
chore: add TODO comment to use mkdir -p with ssh_client
chrisdjscott Aug 25, 2025
af5053c
fix: ensure remote base path using ssh_client mkdir -p
chrisdjscott Aug 25, 2025
5e2d6bb
adding new example
chrisdjscott Aug 25, 2025
c4a9567
run should start in the background
chrisdjscott Aug 25, 2025
b03fcd7
checking job status with paramiko
chrisdjscott Aug 25, 2025
b3b6a8b
typo
chrisdjscott Aug 31, 2025
9792cf8
use run.sl as default for ssh run script too
chrisdjscott Aug 31, 2025
aab1da0
fix tmux has-session command
chrisdjscott Aug 31, 2025
17e0d24
feat: add prompt to reuse existing SSH key pair
chrisdjscott Aug 31, 2025
ba536d9
default to globus with old format config files
chrisdjscott Aug 31, 2025
c1f77d9
add components section in test configs
chrisdjscott Aug 31, 2025
237c5ef
auto fix old config files
chrisdjscott Aug 31, 2025
0370f05
adjust logging levels for config fixes
chrisdjscott Aug 31, 2025
ce4772f
typos in old config section names
chrisdjscott Aug 31, 2025
1cfe00b
feat: load config and validate runner/transferer in authentication
chrisdjscott Sep 4, 2025
35911d4
fix: disallow paramiko SSH runner and SFTP transferer for auth
chrisdjscott Sep 4, 2025
b3120bb
test ssh connection in rjm_config; change default remote dir location
chrisdjscott Sep 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,4 @@ cython_debug/

# setuptools_scm version file
_version.py
.aider*
4 changes: 4 additions & 0 deletions examples/nodeps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dummy*.txt
remote_job.json
stderr.txt
*.log
2 changes: 2 additions & 0 deletions examples/nodeps/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
rm -f {dummy8,dummy24,dummy48}/{dummy.txt,dummy2.txt,remote_job.json,stderr.txt}
1 change: 1 addition & 0 deletions examples/nodeps/dirlist1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy8
3 changes: 3 additions & 0 deletions examples/nodeps/dirlist3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dummy8
dummy24
dummy48
2 changes: 2 additions & 0 deletions examples/nodeps/dummy24/rjm_downloads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dummy.txt
dummy2.txt
1 change: 1 addition & 0 deletions examples/nodeps/dummy24/rjm_uploads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run.sl
10 changes: 10 additions & 0 deletions examples/nodeps/dummy24/run.sl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
#SBATCH --job-name=testfuncx
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=128
#SBATCH --time=00:10:00

touch dummy.txt
sleep 50
touch dummy2.txt
2 changes: 2 additions & 0 deletions examples/nodeps/dummy48/rjm_downloads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dummy.txt
dummy2.txt
1 change: 1 addition & 0 deletions examples/nodeps/dummy48/rjm_uploads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run.sl
10 changes: 10 additions & 0 deletions examples/nodeps/dummy48/run.sl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
#SBATCH --job-name=testfuncx
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=128
#SBATCH --time=00:10:00

touch dummy.txt
sleep 70
touch dummy2.txt
2 changes: 2 additions & 0 deletions examples/nodeps/dummy8/rjm_downloads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dummy.txt
dummy2.txt
1 change: 1 addition & 0 deletions examples/nodeps/dummy8/rjm_uploads.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run.sl
10 changes: 10 additions & 0 deletions examples/nodeps/dummy8/run.sl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
#SBATCH --job-name=testfuncx
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=128
#SBATCH --time=00:05:00

touch dummy.txt
sleep 30
touch dummy2.txt
15 changes: 15 additions & 0 deletions examples/nodeps/run1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

set -e

./cleanup.sh
echo ""
echo "================================================================================"
echo "Start of rjm_batch_submit..."
echo ""
rjm_batch_submit -f dirlist1.txt -ll debug -n
echo ""
echo "================================================================================"
echo "Start of rjm_batch_wait..."
echo ""
rjm_batch_wait -f dirlist1.txt -ll debug -n
7 changes: 7 additions & 0 deletions examples/nodeps/run3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

set -e

./cleanup.sh
rjm_batch_submit -f dirlist3.txt -ll debug
rjm_batch_wait -f dirlist3.txt -ll debug
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies = [
"fair-research-login",
"globus-compute-sdk==3.7.0",
"globus-sdk",
"paramiko",
"requests",
"retry",
]
Expand Down
12 changes: 12 additions & 0 deletions src/rjm/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ def do_authentication(force=False, verbose=False, retry=True):
sys.stderr.write("ERROR: configuration file must be created with rjm_configure before running rjm_authenticate" + os.linesep)
sys.exit(1)

# Load the configuration
config = config_helper.load_config()
runner = config.get("COMPONENTS", "runner")
transferer = config.get("COMPONENTS", "transferer")

# Disallow paramiko SSH runner and SFTP transferer for authentication
if runner == "paramiko_ssh_runner" and transferer == "paramiko_sftp_transferer":
logger.debug("Authentication not required for paramiko SSH")
if verbose:
print("RJM authentication not required for paramiko SSH")
return

# delete token file if exists
if force:
if os.path.isfile(utils.TOKEN_FILE_LOCATION):
Expand Down
157 changes: 112 additions & 45 deletions src/rjm/cli/rjm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def make_parser():
parser.add_argument('-ll', '--loglevel', required=False,
help="level of log verbosity (setting the level here overrides the config file)",
choices=['debug', 'info', 'warn', 'error', 'critical'])
parser.add_argument('-s', '--ssh', action='store_true',
help='Generate an SSH key pair (stored under ~/.rjm) for use with the Paramiko runner. When this option is chosen, Globus Transfer/Compute setup is skipped.')
parser.add_argument('-w', '--where-config', action="store_true", help="Print location of the config file and exit")
parser.add_argument('-v', '--version', action="version", version='%(prog)s ' + __version__)

Expand All @@ -46,6 +48,9 @@ def nesi_setup():
# command line args
parser = make_parser()
args = parser.parse_args()
# Determine whether Globus setup should be performed.
# If SSH option is chosen, we skip Globus (mutually exclusive behavior).
no_globus = args.ssh

if args.where_config:
# print location of config file and exit
Expand All @@ -68,61 +73,121 @@ def nesi_setup():
logger = logging.getLogger(__name__)
logger.info(f"Running rjm_config v{__version__}")

print()
print("="*120)
print()
print("This is an interactive script to configure RJM for accessing NeSI. "
"You will be required to enter information along the way, including your NeSI username and project code.")
print()
print("="*120)
print()
print("At times either a browser window will be automatically opened, or you will be asked to copy a link and open it "
"in a browser, where you will be asked to authenticate and allow RJM to have access. "
"Please ensure the default browser on your system is set to a modern and reasonably up to date browser.")
print()
print("="*120)
print()
print("In some situations a new link will be opened in your browser immediately after you authenticated the last one, "
"which can be easy to miss, so if it looks like nothing is happening, please check your browser window for a pending authentication.")
print()
print("="*120)
print()
if not no_globus:
print()
print("="*120)
print()
print("This is an interactive script to configure RJM for accessing NeSI. "
"You will be required to enter information along the way, including your NeSI username and project code.")
print()
print("="*120)
print()
print("At times either a browser window will be automatically opened, or you will be asked to copy a link and open it "
"in a browser, where you will be asked to authenticate and allow RJM to have access. "
"Please ensure the default browser on your system is set to a modern and reasonably up to date browser.")
print()
print("="*120)
print()
print("In some situations a new link will be opened in your browser immediately after you authenticated the last one, "
"which can be easy to miss, so if it looks like nothing is happening, please check your browser window for a pending authentication.")
print()
print("="*120)
print()

# get extra info from user
username = input(f"Enter NeSI username or press enter to accept default [{getpass.getuser()}]: ").strip() or getpass.getuser()
account = input("Enter NeSI project code or press enter to accept default (you must belong to it) [uoa00106]: ").strip() or "uoa00106"
print("="*120)
# get extra info from user
username = input(f"Enter NeSI username or press enter to accept default [{getpass.getuser()}]: ").strip() or getpass.getuser()
account = input("Enter NeSI project code or press enter to accept default (you must belong to it) [uoa00106]: ").strip() or "uoa00106"
print("="*120)

else:
print()
print("="*120)
print()
print("This is an interactive script to configure RJM for accessing a remote machine via SSH. "
"You will be required to enter information along the way, including your username on the remote machine.")
print()
print("="*120)
print()

# get extra info from user
username = input(f"Enter remote username or press enter to accept default [{getpass.getuser()}]: ").strip() or getpass.getuser()
account = None
print("="*120)

# create the setup object
nesi = NeSISetup(username, account)

# do the globus setup first because it is more interactive
nesi.setup_globus_transfer()
# do the globus setup unless the user asked to skip it
if not no_globus:
# This step is interactive and may open a browser
nesi.setup_globus_transfer()

# If the user asked for an SSH key‑pair, generate it now via the new helper
if args.ssh:
# This will prompt for the remote base path and create the key pair
nesi.setup_paramiko()
paramiko_cfg = nesi.get_paramiko_config()

# write values to config file
req_opts = copy.deepcopy(config_helper.CONFIG_OPTIONS_REQUIRED)
req_opts = copy.deepcopy(config_helper.CONFIG_OPTIONS)

# get config values
globus_ep, globus_path = nesi.get_globus_transfer_config()
funcx_ep = nesi.get_globus_compute_config()
# get config values (only if globus setup was performed)
if not no_globus:
globus_ep, globus_path = nesi.get_globus_transfer_config()
funcx_ep = nesi.get_globus_compute_config()

# modify dict to set values as defaults
done_globus_ep = False
done_globus_path = False
done_funcx_ep = False

# Populate overrides – Globus overrides are applied only when Globus was run.
# Paramiko overrides are applied only when the SSH option was chosen.
for optd in req_opts:
if optd["section"] == "GLOBUS" and optd["name"] == "remote_endpoint":
optd["override"] = globus_ep
done_globus_ep = True
elif optd["section"] == "GLOBUS" and optd["name"] == "remote_path":
optd["override"] = globus_path
done_globus_path = True
elif optd["section"] == "FUNCX" and optd["name"] == "remote_endpoint":
optd["override"] = funcx_ep
done_funcx_ep = True
assert done_globus_ep
assert done_globus_path
assert done_funcx_ep
# ----- Globus overrides (only when Globus setup was run) -----
if not no_globus:
if optd["section"] == "GLOBUS_TRANSFER" and optd["name"] == "remote_endpoint":
optd["override"] = globus_ep
done_globus_ep = True
elif optd["section"] == "GLOBUS_TRANSFER" and optd["name"] == "remote_path":
optd["override"] = globus_path
done_globus_path = True
elif optd["section"] == "GLOBUS_COMPUTE" and optd["name"] == "remote_endpoint":
optd["override"] = funcx_ep
done_funcx_ep = True

# ----- set the transferer and runner based on whether ssh option was chosen or not
if optd["section"] == "COMPONENTS":
if optd["name"] == "runner":
optd["override"] = "globus_compute_slurm_runner"
elif optd["name"] == "transferer":
optd["override"] = "globus_https_transferer"

# ----- Paramiko overrides (only when SSH key pair was generated) -----
if args.ssh:
if optd["section"] == "PARAMIKO":
if optd["name"] == "private_key_file":
optd["override"] = paramiko_cfg["private_key_file"]
elif optd["name"] == "remote_user":
optd["override"] = paramiko_cfg["remote_user"]
elif optd["name"] == "remote_base_path":
optd["override"] = paramiko_cfg["remote_base_path"]
elif optd["name"] == "remote_address":
# Store the remote machine address entered during Paramiko setup
optd["override"] = paramiko_cfg["remote_address"]

# ----- set the transferer and runner based on whether ssh option was chosen or not
if optd["section"] == "COMPONENTS":
if optd["name"] == "runner":
optd["override"] = "paramiko_ssh_runner"
elif optd["name"] == "transferer":
optd["override"] = "paramiko_sftp_transferer"

# sanity checks – only required when Globus overrides were attempted
if not no_globus:
assert done_globus_ep
assert done_globus_path
assert done_funcx_ep

# backup current config if any
if os.path.exists(config_helper.CONFIG_FILE_LOCATION):
Expand All @@ -131,15 +196,17 @@ def nesi_setup():
print("="*120)

# call method to set config file
config_helper.do_configuration(required_options=req_opts, accept_defaults=True)
config_helper.do_configuration(config_options=req_opts)

print("="*120)
print("Configuration file has been updated")
print("="*120)
print("Running authenticate next...")

# force fresh authentication
do_authentication(force=True, verbose=True)
# Run authentication only if Globus steps were not skipped
if not no_globus:
print("Running authenticate next...")
# force fresh authentication
do_authentication(force=True, verbose=True)

print("="*120)
print("You should be ready to start using rjm now")
Expand Down
Loading
Loading