Checkin of 0.2.98 upstream source
[time-slider.git] / usr / share / time-slider / lib / time_slider / zfs.py~
1 #!/usr/bin/python2.6
2 #
3 # CDDL HEADER START
4 #
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
8 #
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
13 #
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 #
20 # CDDL HEADER END
21 #
22
23 import subprocess
24 import re
25 import threading
26 from bisect import insort, bisect_left, bisect_right
27
28 import util
29
30 BYTESPERMB = 1048576
31
32 # Commonly used command paths
33 PFCMD = "/usr/bin/pfexec"
34 ZFSCMD = "/usr/sbin/zfs"
35 ZPOOLCMD = "/usr/sbin/zpool"
36
37
38 class Datasets(Exception):
39     """
40     Container class for all zfs datasets. Maintains a centralised
41     list of datasets (generated on demand) and accessor methods. 
42     Also allows clients to notify when a refresh might be necessary.
43     """
44     # Class wide instead of per-instance in order to avoid duplication
45     filesystems = None
46     volumes = None
47     snapshots = None
48     
49     # Mutex locks to prevent concurrent writes to above class wide
50     # dataset lists.
51     _filesystemslock = threading.Lock()
52     _volumeslock = threading.Lock()
53     snapshotslock = threading.Lock()
54
55     def create_auto_snapshot_set(self, label, tag = None):
56         """
57         Create a complete set of snapshots as if this were
58         for a standard zfs-auto-snapshot operation.
59         
60         Keyword arguments:
61         label:
62             A label to apply to the snapshot name. Cannot be None.
63         tag:
64             A string indicating one of the standard auto-snapshot schedules
65             tags to check (eg. "frequent" for will map to the tag:
66             com.sun:auto-snapshot:frequent). If specified as a zfs property
67             on a zfs dataset, the property corresponding to the tag will 
68             override the wildcard property: "com.sun:auto-snapshot"
69             Default value = None
70         """
71         everything = []
72         included = []
73         excluded = []
74         single = []
75         recursive = []
76         finalrecursive = []
77
78         # Get auto-snap property in two passes. First with the schedule
79         # specific tag override value, then with the general property value
80         cmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume",
81                "-o", "name,com.sun:auto-snapshot", "-s", "name"]
82         if tag:
83             overrideprop = "com.sun:auto-snapshot:" + tag
84             scmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume",
85                     "-o", "name," + overrideprop, "-s", "name"]
86             outdata,errdata = util.run_command(scmd)
87             for line in outdata.rstrip().split('\n'):
88                 line = line.split()
89                 # Skip over unset values. 
90                 if line[1] == "-":
91                     continue
92                 # Add to everything list. This is used later
93                 # for identifying parents/children of a given
94                 # filesystem or volume.
95                 everything.append(line[0])
96                 if line[1] == "true":
97                     included.append(line[0])
98                 elif line[1] == "false":
99                     excluded.append(line[0])
100         # Now use the general property. If no value
101         # was set in the first pass, we set it here.
102         outdata,errdata = util.run_command(cmd)
103         for line in outdata.rstrip().split('\n'):
104             line = line.split()
105             idx = bisect_right(everything, line[0])
106             if len(everything) == 0 or \
107                everything[idx-1] != line[0]:           
108                 # Dataset is neither included nor excluded so far
109                 if line[1] == "-":
110                     continue
111                 everything.insert(idx, line[0])
112                 if line[1] == "true":
113                     included.insert(0, line[0])
114                 elif line[1] == "false":
115                     excluded.append(line[0])
116
117         # Now figure out what can be recursively snapshotted and what
118         # must be singly snapshotted. Single snapshot restrictions apply
119         # to those datasets who have a child in the excluded list.
120         # 'included' is sorted in reverse alphabetical order. 
121         for datasetname in included:
122             excludedchild = False
123             idx = bisect_right(everything, datasetname)
124             children = [name for name in everything[idx:] if \
125                         name.find(datasetname) == 0]
126             for child in children:
127                 idx = bisect_left(excluded, child)
128                 if idx < len(excluded) and excluded[idx] == child:
129                     excludedchild = True
130                     single.append(datasetname)
131                     break
132             if excludedchild == False:
133                 # We want recursive list sorted in alphabetical order
134                 # so insert instead of append to the list.
135                 recursive.insert(0, datasetname)
136
137         for datasetname in recursive:
138             parts = datasetname.rsplit('/', 1)
139             parent = parts[0]
140             if parent == datasetname:
141                 # Root filesystem of the Zpool, so
142                 # this can't be inherited and must be
143                 # set locally.
144                 finalrecursive.append(datasetname)
145                 continue
146             idx = bisect_right(recursive, parent)
147             if len(recursive) > 0 and \
148                recursive[idx-1] == parent:
149                 # Parent already marked for recursive snapshot: so skip
150                 continue
151             else:
152                 finalrecursive.append(datasetname)
153
154         for name in finalrecursive:
155             dataset = ReadWritableDataset(name)
156             dataset.create_snapshot(label, True)
157         for name in single:
158             dataset = ReadWritableDataset(name)
159             dataset.create_snapshot(label, False)
160
161     def list_auto_snapshot_sets(self, tag = None):
162         """
163         Returns a list of zfs filesystems and volumes tagged with
164         the "com.sun:auto-snapshot" property set to "true", either
165         set locally or inherited. Snapshots are excluded from the
166         returned result.
167
168         Keyword Arguments:
169         tag:
170             A string indicating one of the standard auto-snapshot schedules
171             tags to check (eg. "frequent" will map to the tag:
172             com.sun:auto-snapshot:frequent). If specified as a zfs property
173             on a zfs dataset, the property corresponding to the tag will 
174             override the wildcard property: "com.sun:auto-snapshot"
175             Default value = None
176         """
177         #Get auto-snap property in two passes. First with the global
178         #value, then overriding with the label/schedule specific value
179
180         included = []
181         excluded = []
182
183         cmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume",
184                "-o", "name,com.sun:auto-snapshot", "-s", "name"]
185         if tag:
186             overrideprop = "com.sun:auto-snapshot:" + tag
187             scmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume",
188                     "-o", "name," + overrideprop, "-s", "name"]
189             outdata,errdata = util.run_command(scmd)
190             for line in outdata.rstrip().split('\n'):
191                 line = line.split()
192                 if line[1] == "true":
193                     included.append(line[0])
194                 elif line[1] == "false":
195                     excluded.append(line[0])
196         outdata,errdata = util.run_command(cmd)
197         for line in outdata.rstrip().split('\n'):
198             line = line.split()
199             # Only set values that aren't already set. Don't override
200             try:
201                 included.index(line[0])
202                 continue
203             except ValueError:
204                 try:
205                     excluded.index(line[0])
206                     continue
207                 except ValueError:
208                     # Dataset is not listed in either list.
209                     if line[1] == "true":
210                         included.append(line[0])
211         return included
212
213     def list_filesystems(self, pattern = None):
214         """
215         List pattern matching filesystems sorted by name.
216         
217         Keyword arguments:
218         pattern -- Filter according to pattern (default None)
219         """
220         filesystems = []
221         # Need to first ensure no other thread is trying to
222         # build this list at the same time.
223         Datasets._filesystemslock.acquire()
224         if Datasets.filesystems == None:
225             Datasets.filesystems = []
226             cmd = [ZFSCMD, "list", "-H", "-t", "filesystem", \
227                    "-o", "name,mountpoint", "-s", "name"]
228             try:
229                 p = subprocess.Popen(cmd,
230                                      stdout=subprocess.PIPE,
231                                      stderr=subprocess.PIPE,
232                                      close_fds=True)
233                 outdata,errdata = p.communicate()
234                 err = p.wait()
235             except OSError, message:
236                 raise RuntimeError, "%s subprocess error:\n %s" % \
237                                     (cmd, str(message))
238             if err != 0:
239                 Datasets._filesystemslock.release()
240                 raise RuntimeError, '%s failed with exit code %d\n%s' % \
241                                     (str(cmd), err, errdata)
242             for line in outdata.rstrip().split('\n'):
243                 line = line.rstrip().split()
244                 Datasets.filesystems.append([line[0], line[1]])
245         Datasets._filesystemslock.release()
246
247         if pattern == None:
248             filesystems = Datasets.filesystems[:]
249         else:
250             # Regular expression pattern to match "pattern" parameter.
251             regexpattern = ".*%s.*" % pattern
252             patternobj = re.compile(regexpattern)
253
254             for fsname,fsmountpoint in Datasets.filesystems:
255                 patternmatchobj = re.match(patternobj, fsname)
256                 if patternmatchobj != None:
257                     filesystems.append(fsname, fsmountpoint)
258         return filesystems
259
260     def list_volumes(self, pattern = None):
261         """
262         List pattern matching volumes sorted by name.
263         
264         Keyword arguments:
265         pattern -- Filter according to pattern (default None)
266         """
267         volumes = []
268         Datasets._volumeslock.acquire()
269         if Datasets.volumes == None:
270             Datasets.volumes = []
271             cmd = [ZFSCMD, "list", "-H", "-t", "volume", \
272                    "-o", "name", "-s", "name"]
273             try:
274                 p = subprocess.Popen(cmd,
275                                      stdout=subprocess.PIPE,
276                                      stderr=subprocess.PIPE,
277                                      close_fds=True)
278                 outdata,errdata = p.communicate()
279                 err = p.wait()
280             except OSError, message:
281                 raise RuntimeError, "%s subprocess error:\n %s" % \
282                                     (cmd, str(message))
283             if err != 0:
284                 Datasets._volumeslock.release()
285                 raise RuntimeError, '%s failed with exit code %d\n%s' % \
286                                     (str(cmd), err, errdata)
287             for line in outdata.rstrip().split('\n'):
288                 Datasets.volumes.append(line.rstrip())
289         Datasets._volumeslock.release()
290
291         if pattern == None:
292             volumes = Datasets.volumes[:]
293         else:
294             # Regular expression pattern to match "pattern" parameter.
295             regexpattern = ".*%s.*" % pattern
296             patternobj = re.compile(regexpattern)
297
298             for volname in Datasets.volumes:
299                 patternmatchobj = re.match(patternobj, volname)
300                 if patternmatchobj != None:
301                     volumes.append(volname)
302         return volumes
303
304     def list_snapshots(self, pattern = None):
305         """
306         List pattern matching snapshots sorted by creation date.
307         Oldest listed first
308         
309         Keyword arguments:
310         pattern -- Filter according to pattern (default None)
311         """
312         snapshots = []
313         Datasets.snapshotslock.acquire()
314         if Datasets.snapshots == None:
315             Datasets.snapshots = []
316             snaps = []
317             cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value,name", "creation"]
318             try:
319                 p = subprocess.Popen(cmd,
320                                      stdout=subprocess.PIPE,
321                                      stderr=subprocess.PIPE,
322                                      close_fds=True)
323                 outdata,errdata = p.communicate()
324                 err= p.wait()
325             except OSError, message:
326                 Datasets.snapshotslock.release()
327                 raise RuntimeError, "%s subprocess error:\n %s" % \
328                                     (cmd, str(message))
329             if err != 0:
330                 Datasets.snapshotslock.release()
331                 raise RuntimeError, '%s failed with exit code %d\n%s' % \
332                                     (str(cmd), err, errdata)
333             for dataset in outdata.rstrip().split('\n'):
334                 if re.search("@", dataset):
335                     insort(snaps, dataset.split())
336             for snap in snaps:
337                 Datasets.snapshots.append([snap[1], long(snap[0])])
338         if pattern == None:
339             snapshots = Datasets.snapshots[:]
340         else:
341             # Regular expression pattern to match "pattern" parameter.
342             regexpattern = ".*@.*%s" % pattern
343             patternobj = re.compile(regexpattern)
344
345             for snapname,snaptime in Datasets.snapshots:
346                 patternmatchobj = re.match(patternobj, snapname)
347                 if patternmatchobj != None:
348                     snapshots.append([snapname, snaptime])
349         Datasets.snapshotslock.release()
350         return snapshots
351
352     def list_cloned_snapshots(self):
353         """
354         Returns a list of snapshots that have cloned filesystems
355         dependent on them.
356         Snapshots with cloned filesystems can not be destroyed
357         unless dependent cloned filesystems are first destroyed.
358         """
359         cmd = [ZFSCMD, "list", "-H", "-o", "origin"]
360         outdata,errdata = util.run_command(cmd)
361         result = []
362         for line in outdata.rstrip().split('\n'):
363             details = line.rstrip()
364             if details != "-":
365                 try:
366                     result.index(details)
367                 except ValueError:
368                     result.append(details)
369         return result
370
371     def list_held_snapshots(self):
372         """
373         Returns a list of snapshots that have a "userrefs"
374         property value of greater than 0. Resul list is
375         sorted in order of creation time. Oldest listed first.
376         """
377         cmd = [ZFSCMD, "list", "-H",
378                "-t", "snapshot",
379                "-s", "creation",
380                "-o", "userrefs,name"]
381         outdata,errdata = util.run_command(cmd)
382         result = []
383         for line in outdata.rstrip().split('\n'):
384             details = line.split()
385             if details[0] != "0":
386                 result.append(details[1])
387         return result
388
389     def refresh_snapshots(self):
390         """
391         Should be called when snapshots have been created or deleted
392         and a rescan should be performed. Rescan gets deferred until
393         next invocation of zfs.Dataset.list_snapshots()
394         """
395         # FIXME in future.
396         # This is a little sub-optimal because we should be able to modify
397         # the snapshot list in place in some situations and regenerate the 
398         # snapshot list without calling out to zfs(1m). But on the
399         # pro side, we will pick up any new snapshots since the last
400         # scan that we would be otherwise unaware of.
401         Datasets.snapshotslock.acquire()
402         Datasets.snapshots = None
403         Datasets.snapshotslock.release()
404
405
406 class ZPool:
407     """
408     Base class for ZFS storage pool objects
409     """
410     def __init__(self, name):
411         self.name = name
412         self.health = self.__get_health()
413         self.__datasets = Datasets()
414         self.__filesystems = None
415         self.__volumes = None
416         self.__snapshots = None
417
418     def __get_health(self):
419         """
420         Returns pool health status: 'ONLINE', 'DEGRADED' or 'FAULTED'
421         """
422         cmd = [ZPOOLCMD, "list", "-H", "-o", "health", self.name]
423         outdata,errdata = util.run_command(cmd)
424         result = outdata.rstrip()
425         return result
426
427     def get_capacity(self):
428         """
429         Returns the percentage of total pool storage in use.
430         Calculated based on the "used" and "available" properties
431         of the pool's top-level filesystem because the values account
432         for reservations and quotas of children in their calculations,
433         giving a more practical indication of how much capacity is used
434         up on the pool.
435         """
436         if self.health == "FAULTED":
437             raise ZPoolFaultedError("Can not determine capacity of zpool: %s" \
438                                     "because it is in a FAULTED state" \
439                                     % (self.name))
440
441         cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", \
442                "used,available", self.name]
443         outdata,errdata = util.run_command(cmd)
444         _used,_available = outdata.rstrip().split('\n')
445         used = float(_used)
446         available = float(_available) 
447         return 100.0 * used/(used + available)
448
449     def get_available_size(self):
450         """
451         How much unused space is available for use on this Zpool.
452         Answer in bytes.
453         """
454         # zpool(1) doesn't report available space in
455         # units suitable for calulations but zfs(1)
456         # can so use it to find the value for the
457         # filesystem matching the pool.
458         # The root filesystem of the pool is simply
459         # the pool name.
460         poolfs = Filesystem(self.name)
461         avail = poolfs.get_available_size()
462         return avail
463
464     def get_used_size(self):
465         """
466         How much space is in use on this Zpool.
467         Answer in bytes
468         """
469         # Same as ZPool.get_available_size(): zpool(1)
470         # doesn't generate suitable out put so use
471         # zfs(1) on the toplevel filesystem
472         if self.health == "FAULTED":
473             raise ZPoolFaultedError("Can not determine used size of zpool: %s" \
474                                     "because it is in a FAULTED state" \
475                                     % (self.name))
476         poolfs = Filesystem(self.name)
477         used = poolfs.get_used_size()
478         return used
479
480     def list_filesystems(self):
481         """
482         Return a list of filesystems on this Zpool.
483         List is sorted by name.
484         """
485         if self.__filesystems == None:
486             result = []
487             # Provides pre-sorted filesystem list
488             for fsname,fsmountpoint in self.__datasets.list_filesystems():
489                 if re.match(self.name, fsname):
490                     result.append([fsname, fsmountpoint])
491             self.__filesystems = result
492         return self.__filesystems
493
494     def list_volumes(self):
495         """
496         Return a list of volumes (zvol) on this Zpool
497         List is sorted by name
498         """
499         if self.__volumes == None:
500             result = []
501             regexpattern = "^%s" % self.name
502             patternobj = re.compile(regexpattern)
503             for volname in self.__datasets.list_volumes():
504                 patternmatchobj = re.match(patternobj, volname)
505                 if patternmatchobj != None:
506                     result.append(volname)
507             result.sort()
508             self.__volumes = result
509         return self.__volumes
510
511     def list_auto_snapshot_sets(self, tag = None):
512         """
513         Returns a list of zfs filesystems and volumes tagged with
514         the "com.sun:auto-snapshot" property set to "true", either
515         set locally or inherited. Snapshots are excluded from the
516         returned result. Results are not sorted.
517
518         Keyword Arguments:
519         tag:
520             A string indicating one of the standard auto-snapshot schedules
521             tags to check (eg. "frequent" will map to the tag:
522             com.sun:auto-snapshot:frequent). If specified as a zfs property
523             on a zfs dataset, the property corresponding to the tag will 
524             override the wildcard property: "com.sun:auto-snapshot"
525             Default value = None
526         """
527         result = []
528         allsets = self.__datasets.list_auto_snapshot_sets(tag)
529         if len(allsets) == 0:
530             return result
531
532         regexpattern = "^%s" % self.name
533         patternobj = re.compile(regexpattern)
534         for datasetname in allsets:
535             patternmatchobj = re.match(patternobj, datasetname)
536             if patternmatchobj != None:
537                 result.append(datasetname)
538         return result
539
540     def list_snapshots(self, pattern = None):
541         """
542         List pattern matching snapshots sorted by creation date.
543         Oldest listed first
544            
545         Keyword arguments:
546         pattern -- Filter according to pattern (default None)   
547         """
548         # If there isn't a list of snapshots for this dataset
549         # already, create it now and store it in order to save
550         # time later for potential future invocations.
551         Datasets.snapshotslock.acquire()
552         if Datasets.snapshots == None:
553             self.__snapshots = None
554         Datasets.snapshotslock.release()
555         if self.__snapshots == None:
556             result = []
557             regexpattern = "^%s.*@"  % self.name
558             patternobj = re.compile(regexpattern)
559             for snapname,snaptime in self.__datasets.list_snapshots():
560                 patternmatchobj = re.match(patternobj, snapname)
561                 if patternmatchobj != None:
562                     result.append([snapname, snaptime])
563             # Results already sorted by creation time
564             self.__snapshots = result
565         if pattern == None:
566             return self.__snapshots
567         else:
568             snapshots = []
569             regexpattern = "^%s.*@.*%s" % (self.name, pattern)
570             patternobj = re.compile(regexpattern)
571             for snapname,snaptime in self.__snapshots:
572                 patternmatchobj = re.match(patternobj, snapname)
573                 if patternmatchobj != None:
574                     snapshots.append([snapname, snaptime])
575             return snapshots
576
577     def __str__(self):
578         return_string = "ZPool name: " + self.name
579         return_string = return_string + "\n\tHealth: " + self.health
580         try:
581             return_string = return_string + \
582                             "\n\tUsed: " + \
583                             str(self.get_used_size()/BYTESPERMB) + "Mb"
584             return_string = return_string + \
585                             "\n\tAvailable: " + \
586                             str(self.get_available_size()/BYTESPERMB) + "Mb"
587             return_string = return_string + \
588                             "\n\tCapacity: " + \
589                             str(self.get_capacity()) + "%"
590         except ZPoolFaultedError:
591             pass
592         return return_string
593
594
595 class ReadableDataset:
596     """
597     Base class for Filesystem, Volume and Snapshot classes
598     Provides methods for read only operations common to all.
599     """
600     def __init__(self, name, creation = None):
601         self.name = name
602         self.__creationTime = creation
603         self.datasets = Datasets()
604
605     def __str__(self):
606         return_string = "ReadableDataset name: " + self.name + "\n"
607         return return_string
608
609     def get_creation_time(self):
610         if self.__creationTime == None:
611             cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "creation",
612                    self.name]
613             outdata,errdata = util.run_command(cmd)
614             self.__creationTime = long(outdata.rstrip())
615         return self.__creationTime
616
617     def exists(self):
618         """
619         Returns True if the dataset is still existent on the system.
620         False otherwise
621         """
622         # Test existance of the dataset by checking the output of a 
623         # simple zfs get command on the snapshot
624         cmd = [ZFSCMD, "get", "-H", "-o", "name", "type", self.name]
625         try:
626             p = subprocess.Popen(cmd,
627                                  stdout=subprocess.PIPE,
628                                  stderr=subprocess.PIPE,
629                                  close_fds=True)
630             outdata,errdata = p.communicate()
631             err = p.wait()
632         except OSError, message:
633             raise RuntimeError, "%s subprocess error:\n %s" % \
634                             (command, str(message))
635         if err != 0:
636             # Doesn't exist
637             return False
638
639         result = outdata.rstrip()
640         if result == self.name:
641             return True
642         else:
643             return False
644
645     def get_used_size(self):
646         cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "used", self.name]
647         outdata,errdata = util.run_command(cmd)
648         return long(outdata.rstrip())
649
650     def get_user_property(self, prop, local=False):
651         if local == True:
652             cmd = [ZFSCMD, "get", "-s", "local", "-H", "-o", "value", prop, self.name]
653         else:
654             cmd = [ZFSCMD, "get", "-H", "-o", "value", prop, self.name]
655         outdata,errdata = util.run_command(cmd)
656         return outdata.rstrip()
657
658     def set_user_property(self, prop, value):
659         cmd = [PFCMD, ZFSCMD, "set", "%s=%s" % (prop, value), self.name]
660         outdata,errdata = util.run_command(cmd)
661     
662     def unset_user_property(self, prop):
663         cmd = [PFCMD, ZFSCMD, "inherit", prop, self.name]
664         outdata,errdata = util.run_command(cmd)
665
666 class Snapshot(ReadableDataset):
667     """
668     ZFS Snapshot object class.
669     Provides information and operations specfic to ZFS snapshots
670     """    
671     def __init__(self, name, creation = None):
672         """
673         Keyword arguments:
674         name -- Name of the ZFS snapshot
675         creation -- Creation time of the snapshot if known (Default None)
676         """
677         ReadableDataset.__init__(self, name, creation)
678         self.fsname, self.snaplabel = self.__split_snapshot_name()
679         self.poolname = self.__get_pool_name()
680
681     def __get_pool_name(self):
682         name = self.fsname.split("/", 1)
683         return name[0]
684
685     def __split_snapshot_name(self):
686         name = self.name.split("@", 1)
687         # Make sure this is really a snapshot and not a
688         # filesystem otherwise a filesystem could get 
689         # destroyed instead of a snapshot. That would be
690         # really really bad.
691         if name[0] == self.name:
692             raise SnapshotError("\'%s\' is not a valid snapshot name" \
693                                 % (self.name))
694         return name[0],name[1]
695
696     def get_referenced_size(self):
697         """
698         How much unique storage space is used by this snapshot.
699         Answer in bytes
700         """
701         cmd = [ZFSCMD, "get", "-H", "-p", \
702                "-o", "value", "referenced", \
703                self.name]
704         outdata,errdata = util.run_command(cmd)
705         return long(outdata.rstrip())
706
707     def list_children(self):
708         """Returns a recursive list of child snapshots of this snapshot"""
709         cmd = [ZFSCMD,
710                "list", "-t", "snapshot", "-H", "-r", "-o", "name",
711                self.fsname]
712         outdata,errdata = util.run_command(cmd)
713         result = []
714         for line in outdata.rstrip().split('\n'):
715             if re.search("@%s" % (self.snaplabel), line) and \
716                 line != self.name:
717                     result.append(line)
718         return result
719
720     def has_clones(self):
721         """Returns True if the snapshot has any dependent clones"""
722         cmd = [ZFSCMD, "list", "-H", "-o", "origin,name"]
723         outdata,errdata = util.run_command(cmd)
724         for line in outdata.rstrip().split('\n'):
725             details = line.rstrip().split()
726             if details[0] == self.name and \
727                 details[1] != '-':
728                 return True
729         return False
730
731     def destroy(self, deferred=True):
732         """
733         Permanently remove this snapshot from the filesystem
734         Performs deferred destruction by default.
735         """
736         # Be sure it genuninely exists before trying to destroy it
737         if self.exists() == False:
738             return
739         if deferred == False:
740             cmd = [PFCMD, ZFSCMD, "destroy", self.name]
741         else:
742             cmd = [PFCMD, ZFSCMD, "destroy", "-d", self.name]
743
744         outdata,errdata = util.run_command(cmd)
745         # Clear the global snapshot cache so that a rescan will be
746         # triggered on the next call to Datasets.list_snapshots()
747         self.datasets.refresh_snapshots()
748
749     def hold(self, tag):
750         """
751         Place a hold on the snapshot with the specified "tag" string.
752         """
753         # FIXME - fails if hold is already held
754         # Be sure it genuninely exists before trying to place a hold
755         if self.exists() == False:
756             return
757
758         cmd = [PFCMD, ZFSCMD, "hold", tag, self.name]
759         outdata,errdata = util.run_command(cmd)
760
761     def holds(self):
762         """
763         Returns a list of user hold tags for this snapshot
764         """
765         cmd = [ZFSCMD, "holds", self.name]
766         results = []
767         outdata,errdata = util.run_command(cmd)
768
769         for line in outdata.rstrip().split('\n'):
770             if len(line) == 0:
771                 continue
772             # The first line heading columns are  NAME TAG TIMESTAMP
773             # Filter that line out.
774             line = line.split()
775             if (line[0] != "NAME" and line[1] != "TAG"):
776                 results.append(line[1])
777         return results
778
779     def release(self, tag,):
780         """
781         Release the hold on the snapshot with the specified "tag" string.
782         """
783         # FIXME raises exception if no hold exists.
784         # Be sure it genuninely exists before trying to destroy it
785         if self.exists() == False:
786             return
787
788         cmd = [PFCMD, ZFSCMD, "release", tag, self.name]
789
790         outdata,errdata = util.run_command(cmd)
791         # Releasing the snapshot might cause it get automatically
792         # deleted by zfs.
793         # Clear the global snapshot cache so that a rescan will be
794         # triggered on the next call to Datasets.list_snapshots()
795         self.datasets.refresh_snapshots()
796
797
798     def __str__(self):
799         return_string = "Snapshot name: " + self.name
800         return_string = return_string + "\n\tCreation time: " \
801                         + str(self.get_creation_time())
802         return_string = return_string + "\n\tUsed Size: " \
803                         + str(self.get_used_size())
804         return_string = return_string + "\n\tReferenced Size: " \
805                         + str(self.get_referenced_size())
806         return return_string
807
808
809 class ReadWritableDataset(ReadableDataset):
810     """
811     Base class for ZFS filesystems and volumes.
812     Provides methods for operations and properties
813     common to both filesystems and volumes.
814     """
815     def __init__(self, name, creation = None):
816         ReadableDataset.__init__(self, name, creation)
817         self.__snapshots = None
818
819     def __str__(self):
820         return_string = "ReadWritableDataset name: " + self.name + "\n"
821         return return_string
822
823     def get_auto_snap(self, schedule = None):
824         if schedule:
825             cmd = [ZFSCMD, "get", "-H", "-o", "value", \
826                "com.sun:auto-snapshot", self.name]
827         cmd = [ZFSCMD, "get", "-H", "-o", "value", \
828                "com.sun:auto-snapshot", self.name]
829         outdata,errdata = util.run_command(cmd)
830         if outdata.rstrip() == "true":
831             return True
832         else:
833             return False
834
835     def get_available_size(self):
836         cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "available", \
837                self.name]
838         outdata,errdata = util.run_command(cmd)
839         return long(outdata.rstrip())
840
841     def create_snapshot(self, snaplabel, recursive = False):
842         """
843         Create a snapshot for the ReadWritable dataset using the supplied
844         snapshot label.
845
846         Keyword Arguments:
847         snaplabel:
848             A string to use as the snapshot label.
849             The bit that comes after the "@" part of the snapshot
850             name.
851         recursive:
852             Recursively snapshot childfren of this dataset.
853             Default = False
854         """
855         cmd = [PFCMD, ZFSCMD, "snapshot"]
856         if recursive == True:
857             cmd.append("-r")
858         cmd.append("%s@%s" % (self.name, snaplabel))
859         outdata,errdata = util.run_command(cmd, False)
860         if errdata:
861           print errdata
862         self.datasets.refresh_snapshots()
863
864     def list_children(self):
865         
866         # Note, if more dataset types ever come around they will
867         # need to be added to the filsystem,volume args below.
868         # Not for the forseeable future though.
869         cmd = [ZFSCMD, "list", "-H", "-r", "-t", "filesystem,volume",
870                "-o", "name", self.name]
871         outdata,errdata = util.run_command(cmd)
872         result = []
873         for line in outdata.rstrip().split('\n'):
874             if line.rstrip() != self.name:
875                 result.append(line.rstrip())
876         return result
877
878
879     def list_snapshots(self, pattern = None):
880         """
881         List pattern matching snapshots sorted by creation date.
882         Oldest listed first
883            
884         Keyword arguments:
885         pattern -- Filter according to pattern (default None)   
886         """
887         # If there isn't a list of snapshots for this dataset
888         # already, create it now and store it in order to save
889         # time later for potential future invocations.
890         Datasets.snapshotslock.acquire()
891         if Datasets.snapshots == None:
892             self.__snapshots = None
893         Datasets.snapshotslock.release()
894         if self.__snapshots == None:
895             result = []
896             regexpattern = "^%s@" % self.name
897             patternobj = re.compile(regexpattern)
898             for snapname,snaptime in self.datasets.list_snapshots():
899                 patternmatchobj = re.match(patternobj, snapname)
900                 if patternmatchobj != None:
901                     result.append([snapname, snaptime])
902             # Results already sorted by creation time
903             self.__snapshots = result
904         if pattern == None:
905             return self.__snapshots
906         else:
907             snapshots = []
908             regexpattern = "^%s@.*%s" % (self.name, pattern)
909             patternobj = re.compile(regexpattern)
910             for snapname,snaptime in self.__snapshots:
911                 patternmatchobj = re.match(patternobj, snapname)
912                 if patternmatchobj != None:
913                     snapshots.append(snapname)
914             return snapshots
915
916     def set_auto_snap(self, include, inherit = False):
917         if inherit == True:
918             self.unset_user_property("com.sun:auto-snapshot")
919         else:
920             if include == True:
921                 value = "true"
922             else:
923                 value = "false"
924             self.set_user_property("com.sun:auto-snapshot", value)
925
926         return
927
928
929 class Filesystem(ReadWritableDataset):
930     """ZFS Filesystem class"""
931     def __init__(self, name, mountpoint = None):
932         ReadWritableDataset.__init__(self, name)
933         self.__mountpoint = mountpoint
934
935     def __str__(self):
936         return_string = "Filesystem name: " + self.name + \
937                         "\n\tMountpoint: " + self.get_mountpoint() + \
938                         "\n\tMounted: " + str(self.is_mounted()) + \
939                         "\n\tAuto snap: " + str(self.get_auto_snap())
940         return return_string
941
942     def get_mountpoint(self):
943         if (self.__mountpoint == None):
944             cmd = [ZFSCMD, "get", "-H", "-o", "value", "mountpoint", \
945                    self.name]
946             outdata,errdata = util.run_command(cmd)
947             result = outdata.rstrip()
948             self.__mountpoint = result
949         return self.__mountpoint
950
951     def is_mounted(self):
952         cmd = [ZFSCMD, "get", "-H", "-o", "value", "mounted", \
953                self.name]
954         outdata,errdata = util.run_command(cmd)
955         result = outdata.rstrip()
956         if result == "yes":
957             return True
958         else:
959             return False
960
961     def list_children(self):
962         cmd = [ZFSCMD, "list", "-H", "-r", "-t", "filesystem", "-o", "name",
963                self.name]
964         outdata,errdata = util.run_command(cmd)
965         result = []
966         for line in outdata.rstrip().split('\n'):
967             if line.rstrip() != self.name:
968                 result.append(line.rstrip())
969         return result
970
971
972 class Volume(ReadWritableDataset):
973     """
974     ZFS Volume Class
975     This is basically just a stub and does nothing
976     unique from ReadWritableDataset parent class.
977     """
978     def __init__(self, name):
979         ReadWritableDataset.__init__(self, name)
980
981     def __str__(self):
982         return_string = "Volume name: " + self.name + "\n"
983         return return_string
984
985
986 class ZFSError(Exception):
987     """Generic base class for ZPoolFaultedError and SnapshotError
988
989     Attributes:
990         msg -- explanation of the error
991     """
992     def __init__(self, msg):
993         self.msg = msg
994     def __str__(self):
995         return repr(self.msg)
996
997
998 class ZPoolFaultedError(ZFSError):
999     """Exception raised for queries made against ZPools that
1000        are in a FAULTED state
1001
1002     Attributes:
1003         msg -- explanation of the error
1004     """
1005     def __init__(self, msg):
1006         ZFSError.__init__(self, msg)
1007
1008
1009 class SnapshotError(ZFSError):
1010     """Exception raised for invalid snapshot names provided to
1011        Snapshot() constructor.
1012
1013     Attributes:
1014         msg -- explanation of the error
1015     """
1016     def __init__(self, msg):
1017         ZFSError.__init__(self, msg)
1018
1019
1020 def list_zpools():
1021     """Returns a list of all zpools on the system"""
1022     result = []
1023     cmd = [ZPOOLCMD, "list", "-H", "-o", "name"]
1024     outdata,errdata = util.run_command(cmd)
1025     for line in outdata.rstrip().split('\n'):
1026         result.append(line.rstrip())
1027     return result
1028
1029
1030 if __name__ == "__main__":
1031     for zpool in list_zpools():
1032         pool = ZPool(zpool)
1033         print pool
1034         for filesys,mountpoint in pool.list_filesystems():
1035             fs = Filesystem(filesys, mountpoint)
1036             print fs
1037             print "\tSnapshots:"
1038             for snapshot, snaptime in fs.list_snapshots():
1039                 snap = Snapshot(snapshot, snaptime)
1040                 print "\t\t" + snap.name
1041
1042         for volname in pool.list_volumes():
1043             vol = Volume(volname)
1044             print vol
1045             print "\tSnapshots:"
1046             for snapshot, snaptime in vol.list_snapshots():
1047                 snap = Snapshot(snapshot, snaptime)
1048                 print "\t\t" + snap.name
1049