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  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  # #1975 timeout for creating ssh connections 
 53  TIMEOUT_SSH_CONNECT = 30. 
 54   
55 -def ssh_check_known_hosts(ssh, address, port, username=None, logger=None):
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'): #default ubuntu location 73 ssh.load_system_host_keys('/etc/ssh/ssh_known_hosts') 74 except IOError: 75 pass 76 ssh.load_system_host_keys() #default user location 77 except: 78 if logger: 79 logger.error(traceback.format_exc()) 80 # as seen in #767, base64 raises generic Error. 81 # 82 # A corrupt pycrypto build can also cause this, though 83 # the cause of the corrupt builds has been fixed. 84 return "cannot load SSH host keys -- your known_hosts file may be corrupt" 85 86 # #3158: resolve the actual host using the user's ~/.ssh/config 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 # #1849: paramiko will raise an SSHException with an 'Unknown 97 # server' message if the address is not in the known_hosts 98 # file. This causes a lot of confusion to users, so we try 99 # and diagnose this in advance and offer better guidance 100 101 # - ssh.get_host_keys() does not return the system host keys 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
122 -class SSHChildROSLaunchProcess(roslaunch.server.ChildROSLaunchProcess):
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 # env is always empty dict because we only use env_loader 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 # self.is_dead is a flag set by is_alive that affects whether or not we 142 # log errors during a stop(). 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 #load user's ssh configuration 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 #load ssh client and connect 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: #use SSH agent 184 ssh.connect(address, port, username, timeout=TIMEOUT_SSH_CONNECT, key_filename=identity_file) 185 else: #use SSH with login/pass 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 # #1824 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
211 - def start(self):
212 """ 213 Start the remote process. This will create an SSH connection 214 to the remote host. 215 """ 216 self.started = False #won't set to True until we are finished 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
235 - def getapi(self):
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
244 - def is_alive(self):
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 #not started is equivalent to alive in our logic 253 s = self.ssherr 254 s.channel.settimeout(0) 255 try: 256 #drain the pipes 257 data = s.read(2048) 258 if not len(data): 259 self.is_dead = True 260 return False 261 # #2012 il8n: ssh *should* be UTF-8, but often isn't 262 # (e.g. Japan) 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 # #2012: soft fail, printing is not essential. This occurs 271 # with older machines that don't send utf-8 over ssh 272 pass 273 274 s = self.sshout 275 s.channel.settimeout(0) 276 try: 277 #drain the pipes 278 #TODO: write to log file 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 # call the shutdown API first as closing the SSH connection 300 # won't actually kill the process unless it triggers SIGPIPE 301 try: 302 api = self.getapi() 303 if api is not None: 304 #TODO: probably need a timeout on this 305 api.shutdown() 306 except socket.error: 307 # normal if process is already dead 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 # temporary: don't really want to log here as this 315 # may occur during shutdown 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