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