Package duplicity :: Package backends :: Module sshbackend
[hide private]
[frames] | no frames]

Source Code for Module duplicity.backends.sshbackend

  1  # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- 
  2  # 
  3  # Copyright 2002 Ben Escoto <ben@emerose.org> 
  4  # Copyright 2007 Kenneth Loafman <kenneth@loafman.com> 
  5  # 
  6  # This file is part of duplicity. 
  7  # 
  8  # Duplicity is free software; you can redistribute it and/or modify it 
  9  # under the terms of the GNU General Public License as published by the 
 10  # Free Software Foundation; either version 2 of the License, or (at your 
 11  # option) any later version. 
 12  # 
 13  # Duplicity is distributed in the hope that it will be useful, but 
 14  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 16  # General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with duplicity; if not, write to the Free Software Foundation, 
 20  # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 21   
 22  # The following can be redefined to use different shell commands from 
 23  # ssh or scp or to add more arguments.  However, the replacements must 
 24  # have the same syntax.  Also these strings will be executed by the 
 25  # shell, so shouldn't have strange characters in them. 
 26   
 27  import re 
 28  import string 
 29  import time 
 30  import os 
 31   
 32  import duplicity.backend 
 33  from duplicity import globals 
 34  from duplicity import log 
 35  from duplicity import pexpect 
 36  from duplicity.errors import * #@UnusedWildImport 
 37   
38 -class SSHBackend(duplicity.backend.Backend):
39 """This backend copies files using scp. List not supported"""
40 - def __init__(self, parsed_url):
41 """scpBackend initializer""" 42 duplicity.backend.Backend.__init__(self, parsed_url) 43 44 # host string of form [user@]hostname 45 if parsed_url.username: 46 self.host_string = parsed_url.username + "@" + parsed_url.hostname 47 else: 48 self.host_string = parsed_url.hostname 49 # make sure remote_dir is always valid 50 if parsed_url.path: 51 # remove leading '/' 52 self.remote_dir = re.sub(r'^/', r'', parsed_url.path, 1) 53 else: 54 self.remote_dir = '.' 55 self.remote_prefix = self.remote_dir + '/' 56 # maybe use different ssh port 57 if parsed_url.port: 58 globals.ssh_options = globals.ssh_options + " -oPort=%s" % parsed_url.port 59 # set some defaults if user has not specified already. 60 if "ServerAliveInterval" not in globals.ssh_options: 61 globals.ssh_options += " -oServerAliveInterval=%d" % ((int)(globals.timeout / 2)) 62 if "ServerAliveCountMax" not in globals.ssh_options: 63 globals.ssh_options += " -oServerAliveCountMax=2" 64 # set up password 65 if globals.ssh_askpass: 66 self.password = self.get_password() 67 else: 68 if parsed_url.password: 69 self.password = parsed_url.password 70 globals.ssh_askpass = True 71 else: 72 self.password = ''
73
74 - def run_scp_command(self, commandline):
75 """ Run an scp command, responding to password prompts """ 76 for n in range(1, globals.num_retries+1): 77 if n > 1: 78 # sleep before retry 79 time.sleep(30) 80 log.Info("Running '%s' (attempt #%d)" % (commandline, n)) 81 child = pexpect.spawn(commandline, timeout = None) 82 if globals.ssh_askpass: 83 state = "authorizing" 84 else: 85 state = "copying" 86 while 1: 87 if state == "authorizing": 88 match = child.expect([pexpect.EOF, 89 "(?i)timeout, server not responding", 90 "(?i)pass(word|phrase .*):", 91 "(?i)permission denied", 92 "authenticity"]) 93 log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) 94 if match == 0: 95 log.Warn("Failed to authenticate") 96 break 97 elif match == 1: 98 log.Warn("Timeout waiting to authenticate") 99 break 100 elif match == 2: 101 child.sendline(self.password) 102 state = "copying" 103 elif match == 3: 104 log.Warn("Invalid SSH password") 105 break 106 elif match == 4: 107 log.Warn("Remote host authentication failed (missing known_hosts entry?)") 108 break 109 elif state == "copying": 110 match = child.expect([pexpect.EOF, 111 "(?i)timeout, server not responding", 112 "stalled", 113 "authenticity", 114 "ETA"]) 115 log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) 116 if match == 0: 117 break 118 elif match == 1: 119 log.Warn("Timeout waiting for response") 120 break 121 elif match == 2: 122 state = "stalled" 123 elif match == 3: 124 log.Warn("Remote host authentication failed (missing known_hosts entry?)") 125 break 126 elif state == "stalled": 127 match = child.expect([pexpect.EOF, 128 "(?i)timeout, server not responding", 129 "ETA"]) 130 log.Debug("State = %s, Before = '%s'" % (state, child.before.strip())) 131 if match == 0: 132 break 133 elif match == 1: 134 log.Warn("Stalled for too long, aborted copy") 135 break 136 elif match == 2: 137 state = "copying" 138 child.close(force = True) 139 if child.exitstatus == 0: 140 return 141 log.Warn("Running '%s' failed (attempt #%d)" % (commandline, n)) 142 log.Warn("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries)) 143 raise BackendException("Error running '%s'" % commandline)
144
145 - def run_sftp_command(self, commandline, commands):
146 """ Run an sftp command, responding to password prompts, passing commands from list """ 147 maxread = 2000 # expected read buffer size 148 responses = [pexpect.EOF, 149 "(?i)timeout, server not responding", 150 "sftp>", 151 "(?i)pass(word|phrase .*):", 152 "(?i)permission denied", 153 "authenticity", 154 "(?i)no such file or directory", 155 "Couldn't delete file: No such file or directory", 156 "Couldn't delete file", 157 "open(.*): Failure"] 158 max_response_len = max([len(p) for p in responses[1:]]) 159 for n in range(1, globals.num_retries+1): 160 if n > 1: 161 # sleep before retry 162 time.sleep(30) 163 log.Info("Running '%s' (attempt #%d)" % (commandline, n)) 164 child = pexpect.spawn(commandline, timeout = None, maxread=maxread) 165 cmdloc = 0 166 while 1: 167 match = child.expect(responses, 168 searchwindowsize=maxread+max_response_len) 169 log.Debug("State = sftp, Before = '%s'" % (child.before.strip())) 170 if match == 0: 171 break 172 elif match == 1: 173 log.Info("Timeout waiting for response") 174 break 175 if match == 2: 176 if cmdloc < len(commands): 177 command = commands[cmdloc] 178 log.Info("sftp command: '%s'" % (command,)) 179 child.sendline(command) 180 cmdloc += 1 181 else: 182 command = 'quit' 183 child.sendline(command) 184 res = child.before 185 elif match == 3: 186 child.sendline(self.password) 187 elif match == 4: 188 if not child.before.strip().startswith("mkdir"): 189 log.Warn("Invalid SSH password") 190 break 191 elif match == 5: 192 log.Warn("Host key authenticity could not be verified (missing known_hosts entry?)") 193 break 194 elif match == 6: 195 if not child.before.strip().startswith("rm"): 196 log.Warn("Remote file or directory does not exist in command='%s'" % (commandline,)) 197 break 198 elif match == 7: 199 if not child.before.strip().startswith("Removing"): 200 log.Warn("Could not delete file in command='%s'" % (commandline,)) 201 break; 202 elif match == 8: 203 log.Warn("Could not delete file in command='%s'" % (commandline,)) 204 break 205 elif match == 9: 206 log.Warn("Could not open file in command='%s'" % (commandline,)) 207 break 208 child.close(force = True) 209 if child.exitstatus == 0: 210 return res 211 log.Warn("Running '%s' failed (attempt #%d)" % (commandline, n)) 212 log.Warn("Giving up trying to execute '%s' after %d attempts" % (commandline, globals.num_retries)) 213 raise BackendException("Error running '%s'" % commandline)
214
215 - def put(self, source_path, remote_filename = None):
216 if globals.use_scp: 217 self.put_scp(source_path, remote_filename = remote_filename) 218 else: 219 self.put_sftp(source_path, remote_filename = remote_filename)
220
221 - def put_sftp(self, source_path, remote_filename = None):
222 """Use sftp to copy source_dir/filename to remote computer""" 223 if not remote_filename: 224 remote_filename = source_path.get_filename() 225 commands = ["put \"%s\" \"%s.%s.part\"" % 226 (source_path.name, self.remote_prefix, remote_filename), 227 "rename \"%s.%s.part\" \"%s%s\"" % 228 (self.remote_prefix, remote_filename,self.remote_prefix, remote_filename)] 229 commandline = ("%s %s %s" % (globals.sftp_command, 230 globals.ssh_options, 231 self.host_string)) 232 self.run_sftp_command(commandline, commands)
233
234 - def put_scp(self, source_path, remote_filename = None):
235 """Use scp to copy source_dir/filename to remote computer""" 236 if not remote_filename: 237 remote_filename = source_path.get_filename() 238 commandline = "%s %s %s %s:%s%s" % \ 239 (globals.scp_command, globals.ssh_options, source_path.name, self.host_string, 240 self.remote_prefix, remote_filename) 241 self.run_scp_command(commandline)
242
243 - def get(self, remote_filename, local_path):
244 if globals.use_scp: 245 self.get_scp(remote_filename, local_path) 246 else: 247 self.get_sftp(remote_filename, local_path)
248
249 - def get_sftp(self, remote_filename, local_path):
250 """Use sftp to get a remote file""" 251 commands = ["get \"%s%s\" \"%s\"" % 252 (self.remote_prefix, remote_filename, local_path.name)] 253 commandline = ("%s %s %s" % (globals.sftp_command, 254 globals.ssh_options, 255 self.host_string)) 256 self.run_sftp_command(commandline, commands) 257 local_path.setdata() 258 if not local_path.exists(): 259 raise BackendException("File %s not found locally after get " 260 "from backend" % local_path.name)
261
262 - def get_scp(self, remote_filename, local_path):
263 """Use scp to get a remote file""" 264 commandline = "%s %s %s:%s%s %s" % \ 265 (globals.scp_command, globals.ssh_options, self.host_string, self.remote_prefix, 266 remote_filename, local_path.name) 267 self.run_scp_command(commandline) 268 local_path.setdata() 269 if not local_path.exists(): 270 raise BackendException("File %s not found locally after get " 271 "from backend" % local_path.name)
272
273 - def list(self):
274 """ 275 List files available for scp 276 277 Note that this command can get confused when dealing with 278 files with newlines in them, as the embedded newlines cannot 279 be distinguished from the file boundaries. 280 """ 281 dirs = self.remote_dir.split(os.sep) 282 if len(dirs) > 0: 283 if not dirs[0] : 284 dirs = dirs[1:] 285 dirs[0]= '/' + dirs[0] 286 mkdir_commands = []; 287 for d in dirs: 288 mkdir_commands += ["mkdir \"%s\"" % (d)] + ["cd \"%s\"" % (d)] 289 290 commands = mkdir_commands + ["ls -1"] 291 commandline = ("%s %s %s" % (globals.sftp_command, 292 globals.ssh_options, 293 self.host_string)) 294 295 l = self.run_sftp_command(commandline, commands).split('\n')[1:] 296 297 return filter(lambda x: x, map(string.strip, l))
298
299 - def delete(self, filename_list):
300 """ 301 Runs sftp rm to delete files. Files must not require quoting. 302 """ 303 commands = ["cd \"%s\"" % (self.remote_dir,)] 304 for fn in filename_list: 305 commands.append("rm \"%s\"" % fn) 306 commandline = ("%s %s %s" % (globals.sftp_command, globals.ssh_options, self.host_string)) 307 self.run_sftp_command(commandline, commands)
308 309 duplicity.backend.register_backend("ssh", SSHBackend) 310 duplicity.backend.register_backend("scp", SSHBackend) 311 duplicity.backend.register_backend("sftp", SSHBackend) 312