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