Package roslaunch :: Module remoteprocess
[frames] | no frames]

Source Code for Module roslaunch.remoteprocess

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2008, Willow Garage, Inc. 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32  # 
 33  # Revision $Id$ 
 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  # #1975 timeout for creating ssh connections 
 56  TIMEOUT_SSH_CONNECT = 30. 
 57   
58 -def ssh_check_known_hosts(ssh, address, port, username=None, logger=None):
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'): #default ubuntu location 76 ssh.load_system_host_keys('/etc/ssh/ssh_known_hosts') 77 except IOError: 78 pass 79 ssh.load_system_host_keys() #default user location 80 except: 81 if logger: 82 logger.error(traceback.format_exc()) 83 # as seen in #767, base64 raises generic Error. 84 # 85 # A corrupt pycrypto build can also cause this, though 86 # the cause of the corrupt builds has been fixed. 87 return "cannot load SSH host keys -- your known_hosts file may be corrupt" 88 89 # #3158: resolve the actual host using the user's ~/.ssh/config 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 # #1849: paramiko will raise an SSHException with an 'Unknown 100 # server' message if the address is not in the known_hosts 101 # file. This causes a lot of confusion to users, so we try 102 # and diagnose this in advance and offer better guidance 103 104 # - ssh.get_host_keys() does not return the system host keys 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
125 -class SSHChildROSLaunchProcess(roslaunch.server.ChildROSLaunchProcess):
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 # env is always empty dict because we only use env_loader 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 # self.is_dead is a flag set by is_alive that affects whether or not we 145 # log errors during a stop(). 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 #load user's ssh configuration 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 #load ssh client and connect 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 password is None: #use SSH agent 190 ssh.connect(address, port, username, timeout=TIMEOUT_SSH_CONNECT, key_filename=identity_file) 191 else: #use SSH with login/pass 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 # #1824 206 if e[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
217 - def start(self):
218 """ 219 Start the remote process. This will create an SSH connection 220 to the remote host. 221 """ 222 self.started = False #won't set to True until we are finished 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
241 - def getapi(self):
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
250 - def is_alive(self):
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 #not started is equivalent to alive in our logic 259 s = self.ssherr 260 s.channel.settimeout(0) 261 try: 262 #drain the pipes 263 data = s.read(2048) 264 if not len(data): 265 self.is_dead = True 266 return False 267 # #2012 il8n: ssh *should* be UTF-8, but often isn't 268 # (e.g. Japan) 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 # #2012: soft fail, printing is not essential. This occurs 277 # with older machines that don't send utf-8 over ssh 278 pass 279 280 s = self.sshout 281 s.channel.settimeout(0) 282 try: 283 #drain the pipes 284 #TODO: write to log file 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 # call the shutdown API first as closing the SSH connection 306 # won't actually kill the process unless it triggers SIGPIPE 307 try: 308 api = self.getapi() 309 if api is not None: 310 #TODO: probably need a timeout on this 311 api.shutdown() 312 except socket.error: 313 # normal if process is already dead 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 # temporary: don't really want to log here as this 321 # may occur during shutdown 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