Skip to content

Commit 3331e11

Browse files
authored
v24.42.0 (#109)
* Moved counter for checking handler classes to outside the condition (#104) * Add recursive checking when creating a destination directory (#105) * 106 unexplained errors deleting gpg keystore after decryption (#107) * Move exception print statement earlier to avoid confusing log messages. Check for directories before trying to delete them. * Upate changelog * Add way to enable host key validation for SSH/SFTP (#108) * bump version v24.37.2 -> v24.42.0
1 parent 5c3da8d commit 3331e11

20 files changed

Lines changed: 393 additions & 21 deletions

File tree

.vscode/launch.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@
3333
"args": ["-t", "scp-basic", "-c", "test/cfg", "-v3"],
3434
"justMyCode": false
3535
},
36+
{
37+
"name": "Python: Transfer - SFTP Basic",
38+
"type": "debugpy",
39+
"request": "launch",
40+
"preLaunchTask": "Build Test containers",
41+
"program": "src/opentaskpy/cli/task_run.py",
42+
"console": "integratedTerminal",
43+
"args": ["-t", "sftp-basic", "-c", "test/cfg", "-v3"],
44+
"justMyCode": false
45+
},
3646
{
3747
"name": "Python: Transfer - Basic - As job",
3848
"type": "debugpy",

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
# v24.42.0
4+
5+
- Fix issue where different protocols where not being detected properly, and proxy had to be explicitly defined when it was unnecessary
6+
- When creating a destination directory with SFTP, it will now check whether lower level directories exist, and create them if not
7+
- Always check for a directory before trying to delete it and thowing an exception if it doesn't exist.
8+
- Moved exception printing for transfers to earlier in th code to ensure log messages aren't confusing.
9+
- Add ability to check for SSH host key and validate it before proceeding with connection
10+
311
# No release
412

513
- Bump `black` to 24.10.0

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "opentaskpy"
7-
version = "v24.37.2"
7+
version = "v24.42.0"
88
authors = [{ name = "Adam McDonagh", email = "adam@elitemonkey.net" }]
99
license = { text = "GPLv3" }
1010
classifiers = [
@@ -71,7 +71,7 @@ otf-batch-validator = "opentaskpy.cli.batch_validator:main"
7171
profile = 'black'
7272

7373
[tool.bumpver]
74-
current_version = "v24.37.2"
74+
current_version = "v24.42.0"
7575
version_pattern = "vYY.WW.PATCH[-TAG]"
7676
commit_message = "bump version {old_version} -> {new_version}"
7777
commit = true

src/opentaskpy/config/schemas/execution/ssh/protocol.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"port": {
1111
"type": "integer"
1212
},
13+
"hostKeyValidation": {
14+
"type": "boolean"
15+
},
16+
"knownHostsFile": {
17+
"type": "string"
18+
},
1319
"credentials": {
1420
"type": "object",
1521
"properties": {

src/opentaskpy/config/schemas/transfer/sftp_destination/protocol.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"port": {
1111
"type": "integer"
1212
},
13+
"hostKeyValidation": {
14+
"type": "boolean"
15+
},
16+
"knownHostsFile": {
17+
"type": "string"
18+
},
1319
"supportsPosixRename": {
1420
"type": "boolean",
1521
"default": true

src/opentaskpy/config/schemas/transfer/sftp_source/protocol.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"port": {
1111
"type": "integer"
1212
},
13+
"hostKeyValidation": {
14+
"type": "boolean"
15+
},
16+
"knownHostsFile": {
17+
"type": "string"
18+
},
1319
"supportsPosixRename": {
1420
"type": "boolean",
1521
"default": true

src/opentaskpy/config/schemas/transfer/ssh_destination/protocol.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"port": {
1111
"type": "integer"
1212
},
13+
"hostKeyValidation": {
14+
"type": "boolean"
15+
},
16+
"knownHostsFile": {
17+
"type": "string"
18+
},
1319
"credentials": {
1420
"type": "object",
1521
"properties": {

src/opentaskpy/config/schemas/transfer/ssh_source/protocol.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"port": {
1111
"type": "integer"
1212
},
13+
"hostKeyValidation": {
14+
"type": "boolean"
15+
},
16+
"knownHostsFile": {
17+
"type": "string"
18+
},
1319
"credentials": {
1420
"type": "object",
1521
"properties": {

src/opentaskpy/remotehandlers/sftp.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@
1111
from io import StringIO
1212
from shlex import quote
1313

14-
from paramiko import AutoAddPolicy, Channel, RSAKey, SFTPClient, SSHClient
15-
from tenacity import retry, stop_after_attempt, wait_exponential
14+
from paramiko import Channel, RSAKey, SFTPClient, SSHClient
15+
from tenacity import (
16+
retry,
17+
retry_if_exception,
18+
retry_if_not_exception_message,
19+
stop_after_attempt,
20+
wait_exponential,
21+
)
1622

1723
import opentaskpy.otflogging
1824
from opentaskpy.remotehandlers.remotehandler import RemoteTransferHandler
1925

26+
from .ssh_utils import setup_host_key_validation
27+
2028

2129
class SFTPTransfer(RemoteTransferHandler):
2230
"""SFTP Transfer Handler."""
@@ -103,6 +111,12 @@ def connect(self, hostname: str) -> None:
103111
reraise=True,
104112
stop=stop_after_attempt(6),
105113
wait=wait_exponential(multiplier=2, min=5, max=60),
114+
retry=(
115+
retry_if_not_exception_message(
116+
match=r".*(not found in known_hosts|Name or service not known).*"
117+
)
118+
& retry_if_exception(Exception)
119+
),
106120
)
107121
def connect_with_retry(self, client_kwargs: dict) -> SSHClient:
108122
"""Connect to the remote host with retry.
@@ -118,7 +132,7 @@ def connect_with_retry(self, client_kwargs: dict) -> SSHClient:
118132
ssh_client.set_log_channel(
119133
f"{__name__}.{ self.spec['task_id']}.paramiko.transport"
120134
)
121-
ssh_client.set_missing_host_key_policy(AutoAddPolicy())
135+
setup_host_key_validation(ssh_client, self.spec, self.logger)
122136
self.logger.info(f"Connecting to {client_kwargs['hostname']}")
123137

124138
# Set additional timeout options to match the standard timeout
@@ -317,7 +331,20 @@ def push_files_from_worker(
317331
f"[{self.spec['hostname']}] Creating destination directory:"
318332
f" {destination_directory}"
319333
)
320-
self.sftp_client.mkdir(destination_directory)
334+
335+
# We need to check recursively if the destination directory is nested
336+
current_dir = ""
337+
for dir_part in destination_directory.split("/"):
338+
if not dir_part:
339+
continue
340+
current_dir += f"/{dir_part}"
341+
try:
342+
self.sftp_client.stat(current_dir)
343+
except OSError:
344+
self.logger.info(
345+
f"[{self.spec['hostname']}] Destination directory {current_dir} does not exist. Creating it."
346+
)
347+
self.sftp_client.mkdir(current_dir)
321348

322349
# Transfer the files
323350
result = 0

src/opentaskpy/remotehandlers/ssh.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@
1313
from io import StringIO
1414
from shlex import quote
1515

16-
from paramiko import AutoAddPolicy, RSAKey, SFTPClient, SSHClient, Transport
16+
from paramiko import RSAKey, SFTPClient, SSHClient, Transport
1717
from paramiko.channel import ChannelFile, ChannelStderrFile
18-
from tenacity import retry, stop_after_attempt, wait_exponential
18+
from tenacity import (
19+
retry,
20+
retry_if_exception,
21+
retry_if_not_exception_message,
22+
stop_after_attempt,
23+
wait_exponential,
24+
)
1925

2026
import opentaskpy.otflogging
2127
from opentaskpy.exceptions import SSHClientError
@@ -24,6 +30,8 @@
2430
RemoteTransferHandler,
2531
)
2632

33+
from .ssh_utils import setup_host_key_validation
34+
2735
SSH_OPTIONS: str = "-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5"
2836
REMOTE_SCRIPT_BASE_DIR: str = "/tmp" # nosec B108
2937

@@ -52,7 +60,7 @@ def __init__(self, spec: dict):
5260

5361
client = SSHClient()
5462
client.set_log_channel(f"{__name__}.{ spec['task_id']}.paramiko.transport")
55-
client.set_missing_host_key_policy(AutoAddPolicy())
63+
setup_host_key_validation(client, spec, self.logger)
5664
self.ssh_client = client
5765

5866
# Handle default values
@@ -136,6 +144,12 @@ def connect(self, hostname: str, ssh_client: SSHClient | None = None) -> None:
136144
reraise=True,
137145
stop=stop_after_attempt(6),
138146
wait=wait_exponential(multiplier=2, min=5, max=60),
147+
retry=(
148+
retry_if_not_exception_message(
149+
match=r".*(not found in known_hosts|Name or service not known).*"
150+
)
151+
& retry_if_exception(Exception)
152+
),
139153
)
140154
def connect_with_retry(self, ssh_client: SSHClient, kwargs: dict) -> None:
141155
"""Connect to the remote host with retry.
@@ -916,7 +930,8 @@ def __init__(self, remote_host: str, spec: dict):
916930

917931
client = SSHClient()
918932
client.set_log_channel(f"{__name__}.{ spec['task_id']}.paramiko.transport")
919-
client.set_missing_host_key_policy(AutoAddPolicy())
933+
934+
setup_host_key_validation(client, spec, self.logger)
920935

921936
self.ssh_client = client
922937

0 commit comments

Comments
 (0)