Skip to content

Commit eb5d058

Browse files
committed
fix(ns-ha): remove paramiko library
Paramiko requies rust. But the rust build is broken on upstream: openwrt/packages#26644
1 parent b53cf7b commit eb5d058

File tree

2 files changed

+142
-71
lines changed

2 files changed

+142
-71
lines changed

packages/ns-api/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ define Package/ns-api
2121
CATEGORY:=NethSecurity
2222
TITLE:=NethSecurity REST API
2323
URL:=https://github.com/NethServer/nethsecurity-controller/
24-
DEPENDS:=+python3-nethsec +python3-openssl +python3-urllib +python3-idna +python3-requests +python3-paramiko
24+
DEPENDS:=+python3-nethsec +python3-openssl +python3-urllib +python3-idna +python3-requests +sshpass
2525
PKGARCH:=all
2626
endef
2727

packages/ns-api/files/ns.ha

Lines changed: 141 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import hashlib
1717
import time
1818
from nethsec import utils
1919
from jinja2 import Template
20-
import paramiko
21-
import io
20+
import tempfile
2221
import shutil
2322

2423
### Utilities functions
@@ -83,6 +82,103 @@ def get_device_from_ip(uci, ipaddr):
8382
return (n, uci.get('network', n, 'device', default=None))
8483
return (None, None)
8584

85+
def ssh_execute(command, host, port=22, username='root', password=None, private_key_path=None):
86+
"""
87+
Execute SSH command using subprocess
88+
"""
89+
ssh_cmd = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']
90+
# Add port if not default
91+
if port != 22:
92+
ssh_cmd.extend(['-p', str(port)])
93+
# Handle authentication
94+
temp_key_file = None
95+
if private_key_path:
96+
# Convert dropbear key to OpenSSH format
97+
try:
98+
proc = subprocess.run(['dropbearconvert', 'dropbear', 'openssh', private_key_path, '-'],
99+
check=True, capture_output=True, text=True)
100+
# Create temporary file for the converted key
101+
temp_key_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem')
102+
temp_key_file.write(proc.stdout)
103+
temp_key_file.close()
104+
os.chmod(temp_key_file.name, 0o600)
105+
ssh_cmd.extend(['-i', temp_key_file.name])
106+
except subprocess.CalledProcessError as e:
107+
raise utils.ValidationError('ssh_key', f'Failed to convert dropbear key: {e}')
108+
109+
# Add user@host
110+
ssh_cmd.append(f'{username}@{host}')
111+
112+
# Add command
113+
ssh_cmd.append(command)
114+
115+
try:
116+
if password:
117+
# Use sshpass for password authentication
118+
sshpass_cmd = ['sshpass', '-p', password] + ssh_cmd
119+
proc = subprocess.run(sshpass_cmd, capture_output=True, text=True, timeout=30)
120+
else:
121+
proc = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=30)
122+
return proc.stdout, proc.stderr, proc.returncode
123+
except subprocess.TimeoutExpired:
124+
raise utils.ValidationError('ssh_timeout', 'SSH command timed out')
125+
except FileNotFoundError:
126+
if password:
127+
raise utils.ValidationError('sshpass_missing', 'sshpass command not found, cannot use password authentication')
128+
else:
129+
raise utils.ValidationError('ssh_missing', 'ssh command not found')
130+
finally:
131+
# Clean up temporary key file
132+
if temp_key_file and os.path.exists(temp_key_file.name):
133+
os.unlink(temp_key_file.name)
134+
135+
def ssh_upload_file(local_file_path, remote_file_path, host, port=22, username='root', password=None, private_key_path=None):
136+
"""
137+
Upload file via SCP using subprocess
138+
"""
139+
# First create the destination directory
140+
destination_dir = os.path.dirname(remote_file_path)
141+
if destination_dir:
142+
_, _, returncode = ssh_execute(f"mkdir -p {destination_dir}", host, port, username, password, private_key_path)
143+
if returncode != 0:
144+
return False
145+
scp_cmd = ['scp', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']
146+
# Add port if not default
147+
if port != 22:
148+
scp_cmd.extend(['-P', str(port)])
149+
# Handle authentication
150+
temp_key_file = None
151+
if private_key_path:
152+
# Convert dropbear key to OpenSSH format
153+
try:
154+
proc = subprocess.run(['dropbearconvert', 'dropbear', 'openssh', private_key_path, '-'],
155+
check=True, capture_output=True, text=True)
156+
# Create temporary file for the converted key
157+
temp_key_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem')
158+
temp_key_file.write(proc.stdout)
159+
temp_key_file.close()
160+
os.chmod(temp_key_file.name, 0o600)
161+
scp_cmd.extend(['-i', temp_key_file.name])
162+
except subprocess.CalledProcessError as e:
163+
return False
164+
# Add source and destination
165+
scp_cmd.append(local_file_path)
166+
scp_cmd.append(f'{username}@{host}:{remote_file_path}')
167+
try:
168+
if password:
169+
# Use sshpass for password authentication
170+
sshpass_cmd = ['sshpass', '-p', password] + scp_cmd
171+
proc = subprocess.run(sshpass_cmd, capture_output=True, text=True, timeout=60)
172+
else:
173+
proc = subprocess.run(scp_cmd, capture_output=True, text=True, timeout=60)
174+
return proc.returncode == 0
175+
except (subprocess.TimeoutExpired, FileNotFoundError):
176+
return False
177+
finally:
178+
# Clean up temporary key file
179+
if temp_key_file and os.path.exists(temp_key_file.name):
180+
os.unlink(temp_key_file.name)
181+
86182
def allocate_fake_ips(uci):
87183
wan_counter = 0
88184
for n in utils.get_all_by_type(uci, 'network', 'interface'):
@@ -159,59 +255,41 @@ def validate_dhcp(lan_interface):
159255
return errors
160256

161257
def execute_remote_command(command, backup_node_ip=None, port=None):
162-
# Connect to the remote server using SSH
258+
"""Execute a command on the remote backup node using SSH."""
163259
uci = EUci()
164-
ssh = paramiko.SSHClient()
165-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
166260
if port is None:
167261
port = uci.get('dropbear', 'ha_link', 'Port', default=None)
168262
if backup_node_ip is None:
169263
backup_node_ip = uci.get('keepalived', 'ha_peer', 'address', default=None)
170264
if not port or not backup_node_ip:
171265
raise utils.ValidationError('backup_node_ip', 'missing_backup_node_ip')
172-
# Convert the private key from Dropbear to OpenSSH format
173-
private_key_path = '/etc/keepalived/keys/id_rsa'
174-
proc = subprocess.run(['dropbearconvert', 'dropbear', 'openssh', private_key_path, "-"], check=True, capture_output=True, text=True)
175-
private_key = paramiko.RSAKey.from_private_key(io.StringIO(proc.stdout))
176-
ssh.connect(backup_node_ip, port=port, username='root', pkey=private_key)
177-
# Execute the command
178-
_, stdout, stderr = ssh.exec_command(command)
179-
# Read the output
180-
output = stdout.read().decode()
181-
error = stderr.read().decode()
182-
return output, error
266+
stdout, stderr, returncode = ssh_execute(
267+
command,
268+
backup_node_ip,
269+
port=int(port),
270+
private_key_path='/etc/keepalived/keys/id_rsa'
271+
)
272+
if returncode != 0:
273+
raise utils.ValidationError('ssh_command', f'Command failed with return code {returncode}: {stderr}')
183274

184-
def upload_remote_file(local_file_path, remote_file_path, backup_node_ip=None, port=None):
185-
# Prepare the destination directory
186-
destination_dir = os.path.dirname(remote_file_path)
187-
execute_remote_command(f"mkdir -p {destination_dir}")
275+
return stdout, stderr
188276

189-
# Connect to the remote server using SSH
277+
def upload_remote_file(local_file_path, remote_file_path, backup_node_ip=None, port=None):
278+
"""Upload a file to the remote backup node using SCP."""
190279
uci = EUci()
191-
ssh = paramiko.SSHClient()
192-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
193280
if port is None:
194281
port = uci.get('dropbear', 'ha_link', 'Port', default=None)
195282
if backup_node_ip is None:
196283
backup_node_ip = uci.get('keepalived', 'ha_peer', 'address', default=None)
197284
if not port or not backup_node_ip:
198285
raise utils.ValidationError('backup_node_ip', 'missing_backup_node_ip')
199-
200-
# Convert the private key from Dropbear to OpenSSH format
201-
private_key_path = '/etc/keepalived/keys/id_rsa'
202-
proc = subprocess.run(['dropbearconvert', 'dropbear', 'openssh', private_key_path, "-"], check=True, capture_output=True, text=True)
203-
private_key = paramiko.RSAKey.from_private_key(io.StringIO(proc.stdout))
204-
205-
try:
206-
# Upload the file using SFTP
207-
ssh.connect(backup_node_ip, port=port, username='root', pkey=private_key)
208-
sftp = ssh.open_sftp()
209-
sftp.put(local_file_path, remote_file_path)
210-
sftp.close()
211-
ssh.close()
212-
return True
213-
except Exception as e:
214-
return False
286+
return ssh_upload_file(
287+
local_file_path,
288+
remote_file_path,
289+
backup_node_ip,
290+
port=int(port),
291+
private_key_path='/etc/keepalived/keys/id_rsa'
292+
)
215293

216294
def find_device_config(uci, device, config=None):
217295
if config is None:
@@ -677,15 +755,6 @@ def init_remote(ssh_password):
677755
if not all([primary_node_ip, backup_node_ip, password, pubkey]):
678756
raise utils.ValidationError('paramters', 'missing_required_configuration_values')
679757

680-
# Connect to the backup node using paramiko
681-
ssh = paramiko.SSHClient()
682-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
683-
684-
try:
685-
ssh.connect(backup_node_ip, port=22, username='root', password=ssh_password)
686-
except paramiko.SSHException as e:
687-
raise utils.ValidationError('ssh', "ssh_connection_failed")
688-
689758
# Prepare the init-local command
690759
virtual_ip_sections = utils.get_all_by_type(u, 'keepalived', 'ipaddress')
691760
virtual_ip = None
@@ -704,15 +773,18 @@ def init_remote(ssh_password):
704773
"password": password
705774
})
706775

707-
# Execute the init-local command on the backup node
708-
_, stdout, stderr = ssh.exec_command(f"echo '{init_local_command}' | /usr/libexec/rpcd/ns.ha call init-local")
709-
output = stdout.read().decode()
710-
error = stderr.read().decode()
711-
ssh.close()
712-
if error:
713-
raise RuntimeError(f"Error executing init-local on backup node: {error}")
714-
715-
return json.loads(output)
776+
# Execute the init-local command on the backup
777+
stdout, stderr, returncode = ssh_execute(
778+
f"echo '{init_local_command}' | /usr/libexec/rpcd/ns.ha call init-local",
779+
backup_node_ip,
780+
port=22,
781+
password=ssh_password
782+
)
783+
if returncode != 0:
784+
return utils.generic_error(f"ssh_connection_failed: {stderr}")
785+
if stderr:
786+
return utils.generic_error(f"Error executing init-local on backup node: {stderr}")
787+
return json.loads(stdout)
716788

717789

718790
def status():
@@ -762,16 +834,6 @@ def validate_requirements(role, lan_interface, wan_interface):
762834
def check_remote(backup_node_ip, ssh_password, lan_interface, wan_interface):
763835
errors = []
764836

765-
# Accessible via SSH on port 22
766-
ssh = paramiko.SSHClient()
767-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
768-
769-
try:
770-
ssh.connect(backup_node_ip, port=22, username='root', password=ssh_password)
771-
except Exception as e:
772-
errors.append('ssh_connection_failed: ' + str(e))
773-
return {"success": False, "errors": errors}
774-
775837
# Call validate-configuration on the remote node
776838
validate_command = json.dumps({
777839
"role": "backup",
@@ -780,13 +842,22 @@ def check_remote(backup_node_ip, ssh_password, lan_interface, wan_interface):
780842
})
781843

782844
try:
783-
_, stdout, _ = ssh.exec_command(f"echo '{validate_command}' | /usr/libexec/rpcd/ns.ha call validate-requirements")
845+
stdout, stderr, returncode = ssh_execute(
846+
f"echo '{validate_command}' | /usr/libexec/rpcd/ns.ha call validate-requirements",
847+
backup_node_ip,
848+
port=22,
849+
password=ssh_password
850+
)
851+
if returncode != 0:
852+
errors.append('ssh_connection_failed: ' + stderr)
853+
return {"success": False, "errors": errors}
784854
except Exception as e:
785-
errors.append(str(e))
855+
errors.append('ssh_connection_failed: ' + str(e))
856+
return {"success": False, "errors": errors}
786857

787858
try:
788859
if stdout:
789-
result = json.loads(stdout.read().decode())
860+
result = json.loads(stdout)
790861
if not result.get("success"):
791862
errors.extend(result.get("errors", []))
792863
except:

0 commit comments

Comments
 (0)