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