55# Author: Timothy C. Quinn
66# Home: https://github.com/JavaScriptDude/zfslib
77# Licence: https://opensource.org/licenses/BSD-3-Clause
8+ # TODO:
9+ # [.] Allow querying of just zpool properties only rather than digging all zfs list -t all for every call
10+ # - This will make it much faster for such queries
811#########################################
912
1013import subprocess
1821
1922is_py2 = (sys .version_info [0 ] == 2 )
2023
24+ class __DEFAULT__ (object ):pass
25+
2126class Connection :
2227 host = None
2328 _poolset = None
@@ -47,24 +52,16 @@ def __init__(self, host="localhost", trust=False, sshcipher=None, identityfile=N
4752 self .command .extend ([self .host ])
4853
4954
50- # Data is cached unless force=True, snapshots have been made since last read
51- # or properties is different
52- def load_poolset (self , properties = None , get_mounts = True , force = False , _test_data = None ):
53- properties = [] if properties is None else properties
54- if force or self ._dirty or not self ._props_last == properties :
55- self ._poolset ._load (properties = properties , get_mounts = get_mounts , _test_data = _test_data )
56- self ._dirty = False
57- self ._props_last = properties
58-
59- return self ._poolset
60-
55+
56+ # See PoolSet._load for parameters
6157
62- def snapshot_recursively (self , name , snapshotname , properties = None ):
63- properties = {} if properties is None else properties
64- plist = sum ( map ( lambda x : [ '-o' , '%s=%s' % x ], properties . items () ), [] )
65- subprocess . check_call ( self .command + [ "zfs" , "snapshot" , "-r" ] + plist + [ "%s@%s" % ( name , snapshotname )] )
66- self ._dirty = True
58+ def load_poolset (self , zfs_props = None , zpool_props = None , get_mounts = True , force = False , _test_data_zfs = None , _test_data_zpool = None ):
59+ zfs_props = [] if zfs_props is None else zfs_props
60+ if force or not self . _props_last == zfs_props :
61+ self ._poolset . _load ( zfs_props = zfs_props , zpool_props = zpool_props , get_mounts = get_mounts , _test_data_zfs = _test_data_zfs , _test_data_zpool = _test_data_zpool )
62+ self ._props_last = zfs_props
6763
64+ return self ._poolset
6865
6966
7067class PoolSet (object ):
@@ -105,67 +102,96 @@ def lookup(self, name):
105102
106103 return ret
107104
108- # Note: _test_data is for testing only
109- # get_mounts will automated grabbing of mountpoint and mounted properties and
110- # store flag for downstream code to know that these flags are available
111- def _load (self , get_mounts = True , properties = None , _test_data = None ):
105+ # [zfs_props] properties from % zfs list -o <properties>
106+ # [zpool_props] properties from % zpool list -o <properties>
107+ # [get_mounts] Append mountpoint and mounted zfs_props and store flag for downstream code to know that these flags are available
108+ # [_test_data_zfs] testing only
109+ # [_test_data_zpool] testing only
110+ def _load (self , get_mounts = True , zfs_props = None , zpool_props = None , _test_data_zfs = None , _test_data_zpool = None ):
112111 global is_py2
113112
114- _pdef = ['name' , 'creation' ]
113+ # setup zfs list properties (zfs list -o <props>)
114+ _zfs_pdef = ['name' , 'creation' ]
115115
116- if properties is None :
116+ if zfs_props is None :
117117 if get_mounts :
118- _pdef .extend (['mountpoint' , 'mounted' ])
118+ _zfs_pdef .extend (['mountpoint' , 'mounted' ])
119119 self .have_mounts = True
120- properties = _pdef
120+ zfs_props = _zfs_pdef
121121
122122 else :
123- if 'mountpoint' in properties and 'mounted' in properties :
123+ if 'mountpoint' in zfs_props and 'mounted' in zfs_props :
124124 self .have_mounts = True
125125
126126 elif get_mounts :
127- _pdef .extend (['mountpoint' , 'mounted' ])
127+ _zfs_pdef .extend (['mountpoint' , 'mounted' ])
128128 self .have_mounts = True
129129
130130 else :
131131 self .have_mounts = False
132132
133- properties = _pdef + [s for s in properties if not s in _pdef ]
133+ zfs_props = _zfs_pdef + [s for s in zfs_props if not s in _zfs_pdef ]
134+
135+
136+
137+ # setup zpool list properties (zpool list -o <props>)
138+ _zpool_pdef = ['name' , 'size' , 'allocated' , 'free' , 'checkpoint' , 'fragmentation' , 'capacity' , 'health' ]
139+ if zpool_props is None :
140+ zpool_props = _zpool_pdef
141+ else :
142+ zpool_props = _zpool_pdef + [s for s in zpool_props if not s in _zpool_pdef ]
143+
134144
135145 _base_cmd = self .connection .command
136146
137- def extract_properties (s ):
147+ def extract_properties (s , zpool :bool = False ):
148+ props = zpool_props if zpool else zfs_props
138149 if not is_py2 and isinstance (s , bytes ): s = s .decode ('utf-8' )
139150 items = s .strip ().split ( '\t ' )
140- assert len ( items ) == len ( properties ), (properties , items )
141- propvalues = map ( lambda x : None if x == '-' else x , items [ 1 : ] )
142- return [ items [ 0 ], zip ( properties [ 1 : ], propvalues ) ]
151+ assert len ( items ) == len ( props ), (props , items )
152+ propvalues = map ( lambda x : None if x == '-' else x , items [1 :] )
153+ return [ items [ 0 ], zip ( props [ 1 : ], propvalues ) ]
154+
155+ # Gather zfs list data
156+ if _test_data_zfs is None :
157+ zfs_list_output = subprocess .check_output (self .connection .command + ["zfs" , "list" , "-Hpr" , "-o" , "," .join ( zfs_props ), "-t" , "all" ])
158+
159+ else : # Use test data
160+ zfs_list_output = _test_data_zfs
161+
162+ zfs_list_items = OrderedDict ([ extract_properties (s ) for s in zfs_list_output .splitlines () if s .strip () ])
143163
144- if _test_data is None :
145- zfs_list_output = subprocess .check_output (self .connection .command + ["zfs" , "list" , "-Hpr" , "-o" , "," .join ( properties ), "-t" , "all" ])
164+
165+ # Gather zpool list data
166+ if _test_data_zpool is None :
167+ zpool_list_output = subprocess .check_output (self .connection .command + ["zpool" , "list" , "-Hp" , "-o" , "," .join ( zpool_props )])
146168
147169 else : # Use test data
148- zfs_list_output = _test_data
170+ zpool_list_output = _test_data_zpool
171+
172+ zpool_list_items = OrderedDict ([ extract_properties (s , zpool = True ) for s in zpool_list_output .splitlines () if s .strip () ])
149173
150- creations = OrderedDict ([ extract_properties ( s ) for s in zfs_list_output .splitlines () if s .strip () ])
151174
152175 # names of pools
153- old_dsets = [ x .path for x in self .walk () ]
154- old_dsets .reverse ()
155- new_dsets = creations .keys ()
176+ old_items = [ x .path for x in self .walk () ]
177+ old_items .reverse ()
178+ new_items = zfs_list_items .keys ()
156179 pool_cur = None
157180
158- for dset in new_dsets :
159- if "@" in dset :
160- dset , snapshot = dset .split ("@" )
181+ for name in new_items :
182+ is_zpool = False
183+ if "@" in name :
184+ name , snapshot = name .split ("@" )
161185 else :
162186 snapshot = None
163- if "/" not in dset : # pool name
164- if dset not in self ._pools :
165- pool_cur = Pool (dset , self .connection , self .have_mounts )
166- self ._pools [dset ] = pool_cur
167- fs = self ._pools [dset ]
168- poolname , pathcomponents = dset .split ("/" )[0 ], dset .split ("/" )[1 :]
187+ if "/" not in name : # zpool
188+ if name not in self ._pools :
189+ is_zpool = True
190+ pool_cur = Pool (name , self .connection , self .have_mounts )
191+ self ._pools [name ] = pool_cur
192+ fs = self ._pools [name ]
193+
194+ poolname , pathcomponents = name .split ("/" )[0 ], name .split ("/" )[1 :]
169195 fs = self ._pools [poolname ]
170196 for pcomp in pathcomponents :
171197 # traverse the child hierarchy or create if that fails
@@ -177,17 +203,25 @@ def extract_properties(s):
177203 if snapshot not in [ x .name for x in fs .children ]:
178204 fs = Snapshot (pool_cur , snapshot , fs )
179205
180- fs ._properties .update ( creations [fs .path ] )
206+ fs ._properties .update ( zfs_list_items [fs .path ] )
207+
208+ if is_zpool :
209+ # Update with zpool properties
210+ _zpool_props = zpool_list_items .get (name , __DEFAULT__ )
211+ assert _zpool_props != __DEFAULT__ , f"ERROR - zpool '{ name } ' not found in zpool_list_items"
212+ fs ._properties .update ( _zpool_props )
213+
214+ noop ()
181215
182216 # std_avail is avail, std_ref is usedds
183217
184218
185- for dset in old_dsets :
186- if dset not in new_dsets :
187- if "/" not in dset and "@" not in dset : # a pool
188- self .remove (dset )
219+ for name in old_items :
220+ if name not in new_items :
221+ if "/" not in name and "@" not in name : # a pool
222+ self .remove (name )
189223 else :
190- d = self .lookup (dset )
224+ d = self .lookup (name )
191225 d .parent .remove (d )
192226
193227
@@ -1031,3 +1065,7 @@ def f(*popenargs, **kwargs):
10311065
10321066
10331067''' END LEGACY DUCK PUNCHING '''
1068+
1069+ # No operation lambda dropin or breakpoint marker
1070+ def noop (* args , ** kwargs ):
1071+ if len (args ): return args [0 ]
0 commit comments