Update copyright notices to use a central AUTHORS file.
[cumulus.git] / python / cumulus / store / sftp.py
1 # Cumulus: Efficient Filesystem Backup to the Cloud
2 # Copyright (C) 2010 The Cumulus Developers
3 # See the AUTHORS file for a list of contributors.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 #needed for python 2.5
20 from __future__ import with_statement
21
22 from paramiko import Transport, SFTPClient, RSAKey, DSSKey
23 from paramiko.config import SSHConfig
24 import paramiko.util
25 from cumulus.store import Store, type_patterns, NotFoundError
26 import os, os.path
27 import getpass
28 import re
29 import sys
30
31
32 class SSHHostConfig(dict):
33     def __init__(self, hostname, user = None, filename = None):
34         #set defaults
35         if filename == None:
36             filename = os.path.expanduser('~/.ssh/config')
37
38         #read config file
39         ssh_config = SSHConfig()
40         with open(filename) as config_file:
41             ssh_config.parse(config_file)
42
43         self.update(ssh_config.lookup(hostname))
44
45         self.defaults={'port': 22, 'user': getpass.getuser(), 'hostname': hostname, 'hostkeyalias': hostname}
46
47         if user != None:
48             self['user'] = user
49
50     def __getitem__(self, key):
51         if key in self:
52             return dict.__getitem__(self,key)
53         elif key == 'hostkeyalias' and 'hostname' in self:
54             return dict.__getitem__(self,'hostname')
55         else:
56             return self.defaults[key]
57
58
59 class SFTPStore(Store):
60     """implements the sftp:// storage backend
61
62         configuration via openssh/sftp style urls and
63         .ssh/config files
64
65         does not support password authentication or password
66         protected authentication keys"""
67     def __init__(self, url, **kw):
68         if self.netloc.find('@') != -1:
69             user, self.netloc = self.netloc.split('@')
70         else:
71             user = None
72
73         self.config = SSHHostConfig(self.netloc, user)
74
75         host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
76         try:
77             self.hostkey = host_keys[self.config['hostkeyalias']].values()[0]
78         except:
79             print str(self.config)
80             raise
81
82
83         if(self.config.has_key('identityfile')):
84             key_file = os.path.expanduser(self.config['identityfile'])
85             #not really nice but i don't see a cleaner way atm...
86             try:
87                 self.auth_key = RSAKey (key_file)
88             except SSHException, e:
89                 if e.message == 'Unable to parse file':
90                     self.auth_key = DSAKey (key_file)
91                 else:
92                     raise
93         else:
94             filename = os.path.expanduser('~/.ssh/id_rsa')
95             if os.path.exists(filename):
96                 self.auth_key = RSAKey(filename)
97             else:
98                 filename = os.path.expanduser('~/.ssh/id_dsa')
99                 if (os.path.exists(filename)):
100                     self.auth_key = DSSKey (filename)
101
102         self.__connect()
103
104     def __connect(self):
105         self.t = Transport((self.config['hostname'], self.config['port']))
106         self.t.connect(username = self.config['user'], pkey = self.auth_key)
107         self.client = SFTPClient.from_transport(self.t)
108         self.client.chdir(self.path)
109
110     def __build_fn(self, name):
111         return "%s/%s" % (self.path,  name)
112
113     def list(self, type):
114         return filter(type_patterns[type].match, self.client.listdir(self.path))
115
116     def get(self, type, name):
117         return self.client.open(self.__build_fn(name), mode = 'rb')
118
119     def put(self, type, name, fp):
120         remote_file = self.client.open(self.__build_fn(name), mode = 'wb')
121         buf = fp.read(4096)
122         while (len(buf) > 0):
123             remote_file.write(buf)
124             buf = fp.read(4096)
125         remote_file.close()
126
127     def delete(self, type, name):
128         self.client.remove(self.__build_fn(name))
129
130     def stat(self, type, name):
131         try:
132             stat = self.client.stat(self.__build_fn(name))
133             return {'size': stat.st_size}
134         except IOError:
135             raise NotFoundError
136
137     def close(self):
138         """connection has to be explicitly closed, otherwise
139             it will hold the process running idefinitly"""
140         self.client.close()
141         self.t.close()
142
143 Store = SFTPStore