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