Start a proof-of-concept FUSE interface to old snapshots.
[cumulus.git] / contrib / cumulus-fuse
1 #!/usr/bin/python
2 #
3 # FUSE interface to Cumulus, allowing snapshots to be mounted as a virtual
4 # filesystem.
5 #
6 # Copyright (C) 2006-2008  The Regents of the University of California
7 # Written by Michael Vrable <mvrable@cs.ucsd.edu>
8 #
9 # This program can be distributed under the terms of the GNU GPL, either
10 # version 2 of the License, or (at your option) any later version.  See the
11 # file COPYING.
12
13 import itertools, os, stat, errno
14 import fuse
15 from fuse import Fuse
16 import cumulus
17 import cumulus.metadata
18
19 fuse.fuse_python_api = (0, 2)
20
21 # TODO: Figure out FUSE option parsing
22 lowlevel = cumulus.LowlevelDataStore('/backups/lbs/turin')
23 store = cumulus.ObjectStore(lowlevel)
24
25 def _printable(ptr):
26     if ptr is None: return None
27     return tuple(x[1] for x in ptr)
28
29 def parse_path(path):
30     """Strip leading slashe from path, and split apart into components."""
31     if not path.startswith('/'):
32         return None
33     if path == '/':
34         return []
35     else:
36         return path[1:].split('/')
37
38 def load_metadata(path):
39     if type(path) != type([]):
40         path = parse_path(path)
41
42     if path is None or len(path) < 2:
43         return None
44
45     snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
46     metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
47     ptr = metadata.search(lambda x: cmp(x, path[1:]))
48     item = metadata._read(ptr)
49     if metadata._get_path(item) != path[1:]:
50         return None
51     return cumulus.MetadataItem(item, store)
52
53 class MyStat(fuse.Stat):
54     def __init__(self):
55         self.st_mode = 0
56         self.st_ino = 0
57         self.st_dev = 0
58         self.st_nlink = 0
59         self.st_uid = 0
60         self.st_gid = 0
61         self.st_size = 0
62         self.st_atime = 0
63         self.st_mtime = 0
64         self.st_ctime = 0
65
66 class CumulusFS(Fuse):
67     def getattr(self, path):
68         st = MyStat()
69         path = parse_path(path)
70
71         if path is None: return -errno.ENOENT
72         if path == []:
73             # Root directory
74             st.st_mode = stat.S_IFDIR | 0755
75             st.st_nlink = 2
76             return st
77
78         snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
79         if len(path) == 1:
80             # Snapshot directory
81             st.st_mode = stat.S_IFDIR | 0755
82             st.st_nlink = 2
83         else:
84             # File contained within a snapshot
85             m = load_metadata(path)
86             if m is None:
87                 return -errno.ENOENT
88
89             st.st_nlink = 1
90             st.st_uid = m.items.user[0]
91             st.st_gid = m.items.group[0]
92             st.st_mtime = m.items.mtime
93             st.st_ctime = m.items.ctime
94             st.st_atime = m.items.mtime
95             if m.items.type == 'd':
96                 st.st_mode = stat.S_IFDIR | m.items.mode
97                 st.st_nlink = 2
98             elif m.items.type == 'l':
99                 st.st_mode = stat.S_IFLNK | m.items.mode
100             else:
101                 st.st_mode = stat.S_IFREG | m.items.mode
102                 st.st_size = m.items.size
103
104         return st
105
106     def _cumulus_readdir(self, metadata, path):
107         # Find pointer to base directory in metadata
108         ptr1 = metadata.search(lambda x: cmp(x, path))
109
110         # Find pointer to end of directory contents
111         def endcmp(p1):
112             def _cmp(p2):
113                 if len(p2) > len(p1): p2 = p2[0:len(p1)]
114                 if p2 > p1:
115                     return 1
116                 else:
117                     return -1
118             return _cmp
119         ptr2 = metadata.search(endcmp(path), ptr1)
120
121         # Scan through looking for top-level files and directories.  Skip over
122         # data for files in subdirectories.
123         while metadata._cmp(ptr1, ptr2) < 0:
124             item = metadata._read(ptr1)
125             m = cumulus.MetadataItem(item, store)
126             if m.items.name == '.':
127                 itempath = []
128             else:
129                 itempath = m.items.name.split('/')
130             assert itempath[0:len(path)] == path
131
132             if len(itempath) == len(path):
133                 ptr1 = metadata._advance(ptr1)
134                 continue
135
136             if len(itempath) > len(path) + 1:
137                 ptr1 = metadata.search(endcmp(itempath[0:len(path)+1]),
138                                        ptr1, ptr2)
139                 continue
140
141             yield itempath[len(path)]
142             ptr1 = metadata._advance(ptr1)
143
144     def readdir(self, path, offset):
145         if path == '/':
146             for r in itertools.chain(('.', '..'), lowlevel.list_snapshots()):
147                 yield fuse.Direntry(r)
148         else:
149             path = parse_path(path)
150             if path is None:
151                 return
152             snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
153             metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
154             for r in itertools.chain(('.', '..'),
155                                      self._cumulus_readdir(metadata, path[1:])):
156                 yield fuse.Direntry(r)
157
158     def readlink(self, path):
159         m = load_metadata(path)
160         if m is None:
161             return -errno.ENOENT
162         else:
163             return m.items.target
164
165     def open(self, path, flags):
166         m = load_metadata(path)
167         if m is None:
168             return -errno.ENOENT
169         accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
170         if (flags & accmode) != os.O_RDONLY:
171             return -errno.EACCES
172
173     def read(self, path, size, offset):
174         m = load_metadata(path)
175         if m is None:
176             return -errno.ENOENT
177
178         return '\0' * size
179
180 def main():
181     usage="""
182 cumulus-fuse: Mount cumulus snapshots as a filesystem
183
184 """ + Fuse.fusage
185     server = CumulusFS(version="%prog " + fuse.__version__,
186                        usage=usage,
187                        dash_s_do='setsingle')
188
189     server.parser.add_option(mountopt="root", metavar="PATH", default='/',
190                              help="read snapshots from PATH [default: %default]")
191
192     server.parse(errex=1)
193     print server.fuse_args
194     print server.fuse_args.assemble()
195     server.main()
196     store.cleanup()
197
198 if __name__ == '__main__':
199     main()