1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 """
36 Process handler for launching ssh-based roslaunch child processes.
37 """
38
39 import os
40 import socket
41 import traceback
42 try:
43 from xmlrpc.client import ServerProxy
44 except ImportError:
45 from xmlrpclib import ServerProxy
46
47 import rosgraph
48 from roslaunch.core import printlog, printerrlog
49 import roslaunch.pmon
50 import roslaunch.server
51
52 import logging
53 _logger = logging.getLogger("roslaunch.remoteprocess")
54
55
56 TIMEOUT_SSH_CONNECT = 30.
57
59 """
60 Validation routine for loading the host keys and making sure that
61 they are configured properly for the desired SSH. The behavior of
62 this routine can be modified by the ROSLAUNCH_SSH_UNKNOWN
63 environment variable, which enables the paramiko.AutoAddPolicy.
64
65 :param ssh: paramiko SSH client, :class:`paramiko.SSHClient`
66 :param address: SSH IP address, ``str``
67 :param port: SSH port, ``int``
68 :param username: optional username to include in error message if check fails, ``str``
69 :param logger: (optional) logger to record tracebacks to, :class:`logging.Logger`
70 :returns: error message if improperly configured, or ``None``. ``str``
71 """
72 import paramiko
73 try:
74 try:
75 if os.path.isfile('/etc/ssh/ssh_known_hosts'):
76 ssh.load_system_host_keys('/etc/ssh/ssh_known_hosts')
77 except IOError:
78 pass
79 ssh.load_system_host_keys()
80 except:
81 if logger:
82 logger.error(traceback.format_exc())
83
84
85
86
87 return "cannot load SSH host keys -- your known_hosts file may be corrupt"
88
89
90 ssh_config = paramiko.SSHConfig()
91 try:
92 with open(os.path.join(os.path.expanduser('~'), '.ssh', 'config')) as f:
93 ssh_config.parse(f)
94 config_lookup = ssh_config.lookup(address)
95 resolved_address = config_lookup['hostname'] if 'hostname' in config_lookup else address
96 except:
97 resolved_address = address
98
99
100
101
102
103
104
105 hk = ssh._system_host_keys
106 override = os.environ.get('ROSLAUNCH_SSH_UNKNOWN', 0)
107 if override == '1':
108 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
109 elif hk.lookup(resolved_address) is None:
110 port_str = user_str = ''
111 if port != 22:
112 port_str = "-p %s "%port
113 if username:
114 user_str = username+'@'
115 return """%s is not in your SSH known_hosts file.
116
117 Please manually:
118 ssh %s%s%s
119
120 then try roslaunching again.
121
122 If you wish to configure roslaunch to automatically recognize unknown
123 hosts, please set the environment variable ROSLAUNCH_SSH_UNKNOWN=1"""%(resolved_address, port_str, user_str, resolved_address)
124
126 """
127 Process wrapper for launching and monitoring a child roslaunch process over SSH
128 """
129 - def __init__(self, run_id, name, server_uri, machine, master_uri=None):
130 """
131 :param machine: Machine instance. Must be fully configured.
132 machine.env_loader is required to be set.
133 """
134 if not machine.env_loader:
135 raise ValueError("machine.env_loader must have been assigned before creating ssh child instance")
136 args = [machine.env_loader, 'roslaunch', '-c', name, '-u', server_uri, '--run_id', run_id]
137
138 super(SSHChildROSLaunchProcess, self).__init__(name, args, {})
139 self.machine = machine
140 self.master_uri = master_uri
141 self.ssh = self.sshin = self.sshout = self.ssherr = None
142 self.started = False
143 self.uri = None
144
145
146 self.is_dead = False
147
148 - def _ssh_exec(self, command, address, port, username=None, password=None):
149 """
150 :returns: (ssh pipes, message). If error occurs, returns (None, error message).
151 """
152 if self.master_uri:
153 env_command = 'env %s=%s' % (rosgraph.ROS_MASTER_URI, self.master_uri)
154 command = '%s %s' % (env_command, command)
155 try:
156 import Crypto
157 except ImportError as e:
158 _logger.error("cannot use SSH: pycrypto is not installed")
159 return None, "pycrypto is not installed"
160 try:
161 import paramiko
162 except ImportError as e:
163 _logger.error("cannot use SSH: paramiko is not installed")
164 return None, "paramiko is not installed"
165
166 config_block = {'hostname': None, 'user': None, 'identityfile': None}
167 ssh_config = paramiko.SSHConfig()
168 try:
169 with open(os.path.join(os.path.expanduser('~'), '.ssh','config')) as f:
170 ssh_config.parse(f)
171 config_block.update(ssh_config.lookup(address))
172 except:
173 pass
174 address = config_block['hostname'] or address
175 username = username or config_block['user']
176 identity_file = None
177 if config_block.get('identityfile', None):
178 if isinstance(config_block['identityfile'], list):
179 identity_file = [os.path.expanduser(f) for f in config_block['identityfile']]
180 else:
181 identity_file = os.path.expanduser(config_block['identityfile'])
182
183 ssh = paramiko.SSHClient()
184 err_msg = ssh_check_known_hosts(ssh, address, port, username=username, logger=_logger)
185
186 if not err_msg:
187 username_str = '%s@'%username if username else ''
188 try:
189 if not password:
190 ssh.connect(address, port, username, timeout=TIMEOUT_SSH_CONNECT, key_filename=identity_file)
191 else:
192 ssh.connect(address, port, username, password, timeout=TIMEOUT_SSH_CONNECT)
193 except paramiko.BadHostKeyException:
194 _logger.error(traceback.format_exc())
195 err_msg = "Unable to verify host key for remote computer[%s:%s]"%(address, port)
196 except paramiko.AuthenticationException:
197 _logger.error(traceback.format_exc())
198 err_msg = "Authentication to remote computer[%s%s:%s] failed.\nA common cause of this error is a missing key in your authorized_keys file."%(username_str, address, port)
199 except paramiko.SSHException as e:
200 _logger.error(traceback.format_exc())
201 if str(e).startswith("Unknown server"):
202 pass
203 err_msg = "Unable to establish ssh connection to [%s%s:%s]: %s"%(username_str, address, port, e)
204 except socket.error as e:
205
206 if e.args[0] == 111:
207 err_msg = "network connection refused by [%s:%s]"%(address, port)
208 else:
209 err_msg = "network error connecting to [%s:%s]: %s"%(address, port, str(e))
210 if err_msg:
211 return None, err_msg
212 else:
213 printlog("launching remote roslaunch child with command: [%s]"%(str(command)))
214 sshin, sshout, ssherr = ssh.exec_command(command)
215 return (ssh, sshin, sshout, ssherr), "executed remotely"
216
218 """
219 Start the remote process. This will create an SSH connection
220 to the remote host.
221 """
222 self.started = False
223 self.ssh = self.sshin = self.sshout = self.ssherr = None
224 with self.lock:
225 name = self.name
226 m = self.machine
227 if m.user is not None:
228 printlog("remote[%s]: creating ssh connection to %s:%s, user[%s]"%(name, m.address, m.ssh_port, m.user))
229 else:
230 printlog("remote[%s]: creating ssh connection to %s:%s"%(name, m.address, m.ssh_port))
231 _logger.info("remote[%s]: invoking with ssh exec args [%s]"%(name, ' '.join(self.args)))
232 sshvals, msg = self._ssh_exec(' '.join(self.args), m.address, m.ssh_port, m.user, m.password)
233 if sshvals is None:
234 printerrlog("remote[%s]: failed to launch on %s:\n\n%s\n\n"%(name, m.name, msg))
235 return False
236 self.ssh, self.sshin, self.sshout, self.ssherr = sshvals
237 printlog("remote[%s]: ssh connection created"%name)
238 self.started = True
239 return True
240
242 """
243 :returns: ServerProxy to remote client XMLRPC server, `ServerProxy`
244 """
245 if self.uri:
246 return ServerProxy(self.uri)
247 else:
248 return None
249
251 """
252 :returns: ``True`` if the process is alive. is_alive needs to be
253 called periodically as it drains the SSH buffer, ``bool``
254 """
255 if self.started and not self.ssh:
256 return False
257 elif not self.started:
258 return True
259 s = self.ssherr
260 s.channel.settimeout(0)
261 try:
262
263 data = s.read(2048)
264 if not len(data):
265 self.is_dead = True
266 return False
267
268
269 data = data.decode('utf-8')
270 printerrlog("remote[%s]: %s"%(self.name, data))
271 except socket.timeout:
272 pass
273 except IOError:
274 return False
275 except UnicodeDecodeError:
276
277
278 pass
279
280 s = self.sshout
281 s.channel.settimeout(0)
282 try:
283
284
285 data = s.read(2048)
286 if not len(data):
287 self.is_dead = True
288 return False
289 except socket.timeout:
290 pass
291 except IOError:
292 return False
293 return True
294
295 - def stop(self, errors=None):
296 """
297 Terminate this process, including the SSH connection.
298 """
299 if errors is None:
300 errors = []
301 with self.lock:
302 if not self.ssh:
303 return
304
305
306
307 try:
308 api = self.getapi()
309 if api is not None:
310
311 api.shutdown()
312 except socket.error:
313
314 address, port = self.machine.address, self.machine.ssh_port
315 if not self.is_dead:
316 printerrlog("remote[%s]: unable to contact [%s] to shutdown remote processes!"%(self.name, address))
317 else:
318 printlog("remote[%s]: unable to contact [%s] to shutdown cleanly. The remote roslaunch may have exited already."%(self.name, address))
319 except:
320
321
322 traceback.print_exc()
323
324 _logger.info("remote[%s]: closing ssh connection", self.name)
325 self.sshin.close()
326 self.sshout.close()
327 self.ssherr.close()
328 self.ssh.close()
329
330 self.sshin = None
331 self.sshout = None
332 self.ssherr = None
333 self.ssh = None
334 _logger.info("remote[%s]: ssh connection closed", self.name)
335