@@ -17,8 +17,7 @@ import hashlib
1717import time
1818from nethsec import utils
1919from jinja2 import Template
20- import paramiko
21- import io
20+ import tempfile
2221import 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+
86182def 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
161257def 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
216294def 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
718790def status ():
@@ -762,16 +834,6 @@ def validate_requirements(role, lan_interface, wan_interface):
762834def 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