1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | # $URL: baculafs/trunk/baculafs.py $ |
---|
5 | # $Id: baculafs.py 787 2009-08-27 21:54:48Z joergs $ |
---|
6 | |
---|
7 | import fuse |
---|
8 | import logging |
---|
9 | import stat |
---|
10 | import errno |
---|
11 | import os |
---|
12 | import pexpect |
---|
13 | import sys |
---|
14 | import re |
---|
15 | |
---|
16 | fuse.fuse_python_api = (0, 2) |
---|
17 | |
---|
18 | ###### bconsole |
---|
19 | ################ |
---|
20 | |
---|
21 | BACULA_FS_VERSION = "$Rev: 787 $" |
---|
22 | |
---|
23 | LOG_FILENAME = '/tmp/baculafs.log' |
---|
24 | #LOG_BCONSOLE = '/tmp/bconsole.log' |
---|
25 | LOG_BCONSOLE_DUMP = '/tmp/bconsole.out' |
---|
26 | |
---|
27 | |
---|
28 | #BACULA_CMD = 'strace -f -o /tmp/bconsole.strace /usr/sbin/bconsole -n' |
---|
29 | BACULA_CMD = '/usr/sbin/bconsole -n' |
---|
30 | |
---|
31 | BCONSOLE_CMD_PROMPT='\*' |
---|
32 | BCONSOLE_SELECT_PROMPT='Select .*:.*' |
---|
33 | BCONSOLE_RESTORE_PROMPT='\$ ' |
---|
34 | |
---|
35 | # define the states bconsole can be in |
---|
36 | class BconsoleState: |
---|
37 | UNKNOWN = '' |
---|
38 | CMD_PROMPT = BCONSOLE_CMD_PROMPT |
---|
39 | SELECT_PROMPT = BCONSOLE_SELECT_PROMPT |
---|
40 | RESTORE_PROMPT = BCONSOLE_RESTORE_PROMPT |
---|
41 | ERROR = "error" |
---|
42 | |
---|
43 | # direct access to the program bconsole |
---|
44 | class Bconsole: |
---|
45 | |
---|
46 | def __init__(self): |
---|
47 | #logging.debug('BC init') |
---|
48 | |
---|
49 | self.cwd = "/" |
---|
50 | self.state = BconsoleState.UNKNOWN |
---|
51 | self.last_items_dict = {} |
---|
52 | |
---|
53 | self.bconsole = pexpect.spawn( BACULA_CMD, logfile=file(LOG_BCONSOLE_DUMP, 'w'), timeout=10 ) |
---|
54 | self.bconsole.setecho( False ) |
---|
55 | self.bconsole.expect( BCONSOLE_CMD_PROMPT ) |
---|
56 | self.bconsole.sendline( 'restore' ) |
---|
57 | self.bconsole.expect( BCONSOLE_SELECT_PROMPT ) |
---|
58 | self.bconsole.sendline( "5" ) |
---|
59 | self.wait_for_prompt() |
---|
60 | |
---|
61 | |
---|
62 | def _normalize_dir( self, path ): |
---|
63 | # guarantee that directory path ends with (exactly one) "/" |
---|
64 | return path.rstrip('/') + "/" |
---|
65 | |
---|
66 | def _get_select_items(self,lines): |
---|
67 | re_select_items = re.compile('\s*[0-9]+:\s*.+') |
---|
68 | return filter( re_select_items.match, lines ) |
---|
69 | |
---|
70 | def _get_item_dict( self,lines ): |
---|
71 | dictionary = {} |
---|
72 | for line in self._get_select_items(lines): |
---|
73 | number,item = line.split( ":", 1 ) |
---|
74 | item = self._normalize_dir( item.strip() ) |
---|
75 | number = number.strip() |
---|
76 | dictionary[item]=number |
---|
77 | return dictionary |
---|
78 | |
---|
79 | |
---|
80 | |
---|
81 | def wait_for_prompt(self): |
---|
82 | |
---|
83 | # set to error state. |
---|
84 | # Only set back to valid state, |
---|
85 | # if a valid prompt is received |
---|
86 | self.state=BconsoleState.ERROR |
---|
87 | |
---|
88 | try: |
---|
89 | index = self.bconsole.expect( [ BCONSOLE_SELECT_PROMPT, BCONSOLE_RESTORE_PROMPT ] ) |
---|
90 | if index == 0: |
---|
91 | # SELECT_PROMPT |
---|
92 | self.state=BconsoleState.SELECT_PROMPT |
---|
93 | lines = self.bconsole.before.splitlines() |
---|
94 | self.last_items_dict = self._get_item_dict(lines) |
---|
95 | logging.debug( str( self.last_items_dict ) ) |
---|
96 | elif index == 1: |
---|
97 | # RESTORE_PROMPT |
---|
98 | self.state=BconsoleState.RESTORE_PROMPT |
---|
99 | else: |
---|
100 | logging.error( "unexpected result" ) |
---|
101 | except pexpect.EOF: |
---|
102 | logging.error( "EOF bconsole" ) |
---|
103 | except pexpect.TIMEOUT: |
---|
104 | logging.error( "TIMEOUT bconsole" ) |
---|
105 | |
---|
106 | return self.state |
---|
107 | |
---|
108 | |
---|
109 | |
---|
110 | def cd(self,path): |
---|
111 | path = self._normalize_dir( path ) |
---|
112 | logging.debug( "(" + path + ")" ) |
---|
113 | |
---|
114 | # parse for BCONSOLE_SELECT_PROMPT or BCONSOLE_RESTORE_PROMPT |
---|
115 | # BCONSOLE_SELECT_PROMPT: take first part of path and try to match. send number. iterate |
---|
116 | # BCONSOLE_RESTORE_PROMPT: cd to directory (as before) |
---|
117 | |
---|
118 | if not path: |
---|
119 | return True |
---|
120 | |
---|
121 | if path == "/": |
---|
122 | return True |
---|
123 | |
---|
124 | if self.state == BconsoleState.SELECT_PROMPT: |
---|
125 | return self.cd_select( path ) |
---|
126 | elif self.state == BconsoleState.RESTORE_PROMPT: |
---|
127 | return self.cd_restore( path ) |
---|
128 | # else error |
---|
129 | return False |
---|
130 | |
---|
131 | |
---|
132 | def cd_select(self, path): |
---|
133 | logging.debug( "(" + path + ")" ) |
---|
134 | |
---|
135 | # get top level directory |
---|
136 | directory,sep,path=path.lstrip( "/" ).partition( "/" ) |
---|
137 | directory=self._normalize_dir(directory) |
---|
138 | logging.debug( "directory: " + directory ) |
---|
139 | |
---|
140 | if self.last_items_dict[directory]: |
---|
141 | logging.debug( "directory: " + directory + " (" + self.last_items_dict[directory] + ")" ) |
---|
142 | self.bconsole.sendline( self.last_items_dict[directory] ) |
---|
143 | self.wait_for_prompt() |
---|
144 | self.cd( path ) |
---|
145 | return True |
---|
146 | |
---|
147 | return False |
---|
148 | |
---|
149 | |
---|
150 | |
---|
151 | def cd_restore(self, path): |
---|
152 | logging.debug( "(" + path + ")" ) |
---|
153 | |
---|
154 | self.bconsole.sendline( 'cd ' + path ) |
---|
155 | #self.bconsole.expect( BCONSOLE_RESTORE_PROMPT, timeout=10 ) |
---|
156 | #self.bconsole.sendline( 'pwd' ) |
---|
157 | |
---|
158 | index = self.bconsole.expect( ["cwd is: " + path + "[/]?", BCONSOLE_RESTORE_PROMPT, pexpect.EOF, pexpect.TIMEOUT ] ) |
---|
159 | logging.debug( "cd result: " + str(index) ) |
---|
160 | |
---|
161 | if index == 0: |
---|
162 | # path ok, now wait for prompt |
---|
163 | self.bconsole.expect( BCONSOLE_RESTORE_PROMPT ) |
---|
164 | return True |
---|
165 | elif index == 1: |
---|
166 | #print "wrong path" |
---|
167 | return False |
---|
168 | elif index == 2: |
---|
169 | logging.error( "EOF bconsole" ) |
---|
170 | #raise? |
---|
171 | return False |
---|
172 | elif index == 3: |
---|
173 | logging.error( "TIMEOUT bconsole" ) |
---|
174 | return False |
---|
175 | |
---|
176 | def ls(self, path): |
---|
177 | logging.debug( "(" + path + ")" ) |
---|
178 | |
---|
179 | if self.cd( path ): |
---|
180 | if self.state == BconsoleState.SELECT_PROMPT: |
---|
181 | return self.last_items_dict.keys() |
---|
182 | elif self.state == BconsoleState.RESTORE_PROMPT: |
---|
183 | return self.ls_restore( path ) |
---|
184 | else: |
---|
185 | return |
---|
186 | |
---|
187 | |
---|
188 | def ls_restore( self, path ): |
---|
189 | self.bconsole.sendline( 'ls' ) |
---|
190 | self.bconsole.expect( BCONSOLE_RESTORE_PROMPT ) |
---|
191 | lines = self.bconsole.before.splitlines() |
---|
192 | #logging.debug( str(lines) ) |
---|
193 | return lines |
---|
194 | |
---|
195 | |
---|
196 | ############### |
---|
197 | |
---|
198 | class BaculaFS(fuse.Fuse): |
---|
199 | |
---|
200 | TYPE_NONE = 0 |
---|
201 | TYPE_FILE = 1 |
---|
202 | TYPE_DIR = 2 |
---|
203 | |
---|
204 | files = { '': {'type': TYPE_DIR} } |
---|
205 | |
---|
206 | def __init__(self, *args, **kw): |
---|
207 | logging.debug('init') |
---|
208 | #self.console = Bconsole() |
---|
209 | fuse.Fuse.__init__(self, *args, **kw) |
---|
210 | #logging.debug('init finished') |
---|
211 | |
---|
212 | |
---|
213 | def _getattr(self,path): |
---|
214 | # TODO: may cause problems with filenames that ends with "/" |
---|
215 | path = path.rstrip( '/' ) |
---|
216 | logging.debug( '"' + path + '"' ) |
---|
217 | |
---|
218 | if (path in self.files): |
---|
219 | #logging.debug( "(" + path + ")=" + str(self.files[path]) ) |
---|
220 | return self.files[path] |
---|
221 | |
---|
222 | if Bconsole().cd(path): |
---|
223 | # don't define files, because these have not been checked |
---|
224 | self.files[path] = { 'type': self.TYPE_DIR, 'dirs': [ ".", ".." ] } |
---|
225 | |
---|
226 | return self.files[path] |
---|
227 | |
---|
228 | # TODO: only works after readdir for the directory (eg. ls) |
---|
229 | def getattr(self, path): |
---|
230 | |
---|
231 | # TODO: may cause problems with filenames that ends with "/" |
---|
232 | path = path.rstrip( '/' ) |
---|
233 | logging.debug( '"' + path + '"' ) |
---|
234 | |
---|
235 | st = fuse.Stat() |
---|
236 | |
---|
237 | if not (path in self.files): |
---|
238 | self._getattr(path) |
---|
239 | |
---|
240 | if not (path in self.files): |
---|
241 | return -errno.ENOENT |
---|
242 | |
---|
243 | file = self.files[path] |
---|
244 | |
---|
245 | if file['type'] == self.TYPE_FILE: |
---|
246 | st.st_mode = stat.S_IFREG | 0444 |
---|
247 | st.st_nlink = 1 |
---|
248 | st.st_size = 0 |
---|
249 | return st |
---|
250 | elif file['type'] == self.TYPE_DIR: |
---|
251 | st.st_mode = stat.S_IFDIR | 0755 |
---|
252 | if 'dirs' in file: |
---|
253 | st.st_nlink = len(file['dirs']) |
---|
254 | else: |
---|
255 | st.st_nlink = 2 |
---|
256 | return st |
---|
257 | |
---|
258 | # TODO: check for existens |
---|
259 | return -errno.ENOENT |
---|
260 | |
---|
261 | |
---|
262 | |
---|
263 | def _getdir(self, path): |
---|
264 | |
---|
265 | # TODO: may cause problems with filenames that ends with "/" |
---|
266 | path = path.rstrip( '/' ) |
---|
267 | logging.debug( '"' + path + '"' ) |
---|
268 | |
---|
269 | if (path in self.files): |
---|
270 | #logging.debug( "(" + path + ")=" + str(self.files[path]) ) |
---|
271 | if self.files[path]['type'] == self.TYPE_NONE: |
---|
272 | logging.info( '"' + path + '" does not exist (cached)' ) |
---|
273 | return self.files[path] |
---|
274 | elif self.files[path]['type'] == self.TYPE_FILE: |
---|
275 | logging.info( '"' + path + '"=file (cached)' ) |
---|
276 | return self.files[path] |
---|
277 | elif ((self.files[path]['type'] == self.TYPE_DIR) and ('files' in self.files[path])): |
---|
278 | logging.info( '"' + path + '"=dir (cached)' ) |
---|
279 | return self.files[path] |
---|
280 | |
---|
281 | try: |
---|
282 | files = Bconsole().ls(path) |
---|
283 | logging.debug( " files: " + str( files ) ) |
---|
284 | |
---|
285 | # setting initial empty directory. Add entires later in this function |
---|
286 | self.files[path] = { 'type': self.TYPE_DIR, 'dirs': [ ".", ".." ], 'files': [] } |
---|
287 | for i in files: |
---|
288 | if i.endswith('/'): |
---|
289 | # we expect a directory |
---|
290 | # TODO: error with filesnames, that ends with '/' |
---|
291 | i = i.rstrip( '/' ) |
---|
292 | self.files[path]['dirs'].append(i) |
---|
293 | if not (i in self.files): |
---|
294 | self.files[path + "/" + i] = { 'type': self.TYPE_DIR } |
---|
295 | else: |
---|
296 | self.files[path]['files'].append(i) |
---|
297 | self.files[path + "/" + i] = { 'type': self.TYPE_FILE } |
---|
298 | |
---|
299 | except Exception as e: |
---|
300 | logging.exception(e) |
---|
301 | logging.error( "no access to path " + path ) |
---|
302 | self.files[path] = { 'type': self.TYPE_NONE } |
---|
303 | |
---|
304 | logging.debug( '"' + path + '"=' + str( self.files[path] ) ) |
---|
305 | return self.files[path] |
---|
306 | |
---|
307 | |
---|
308 | |
---|
309 | def readdir(self, path, offset): |
---|
310 | logging.debug( '"' + path + '", offset=' + str(offset) + ')' ) |
---|
311 | |
---|
312 | dir = self._getdir( path ) |
---|
313 | |
---|
314 | #logging.debug( " readdir: type: " + str( dir['type'] ) ) |
---|
315 | #logging.debug( " readdir: dirs: " + str( dir['dirs'] ) ) |
---|
316 | #logging.debug( " readdir: file: " + str( dir['files'] ) ) |
---|
317 | |
---|
318 | if dir['type'] != self.TYPE_DIR: |
---|
319 | return -errno.ENOENT |
---|
320 | else: |
---|
321 | return [fuse.Direntry(f) for f in dir['files'] + dir['dirs']] |
---|
322 | |
---|
323 | #def open( self, path, flags ): |
---|
324 | #logging.debug( "open " + path ) |
---|
325 | #return -errno.ENOENT |
---|
326 | |
---|
327 | #def read( self, path, length, offset): |
---|
328 | #logging.debug( "read " + path ) |
---|
329 | #return -errno.ENOENT |
---|
330 | |
---|
331 | if __name__ == "__main__": |
---|
332 | # initialize logging |
---|
333 | logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG,format="%(asctime)s %(process)5d(%(threadName)s) %(levelname)-7s %(funcName)s( %(message)s )") |
---|
334 | |
---|
335 | |
---|
336 | usage = """ |
---|
337 | Bacula filesystem: displays files from Bacula backups as a (userspace) filesystem. |
---|
338 | Internaly, it uses Baculas bconsole. |
---|
339 | |
---|
340 | """ + fuse.Fuse.fusage |
---|
341 | |
---|
342 | |
---|
343 | fs = BaculaFS( |
---|
344 | version="%prog: " + BACULA_FS_VERSION, |
---|
345 | usage=usage, |
---|
346 | # required to let "-s" set single-threaded execution |
---|
347 | dash_s_do='setsingle' |
---|
348 | ) |
---|
349 | |
---|
350 | #server.parser.add_option(mountopt="root", metavar="PATH", default='/',help="mirror filesystem from under PATH [default: %default]") |
---|
351 | #server.parse(values=server, errex=1) |
---|
352 | fs.parse() |
---|
353 | |
---|
354 | fs.main() |
---|