Skip to content

Commit 50bf74b

Browse files
Enumerate zpool Properties. API Updated
Updated to automatically enumerate `zpool` properties as well as allowing specifying other non-defaulted parameters. API of methods have changed. Updating to 0.10.0 to clearly flag new API.
1 parent 6329927 commit 50bf74b

14 files changed

+1388
-120
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ See examples folder
2828
2929
3030
# Load poolset.
31-
# Properties can be queried here with: properties=['prop1','prop2',...]
31+
# zfs properties can be queried here with: zfs_prop=['prop1','prop2',...]
32+
# zpool properties can be queried here with: zpool_props=['prop1','prop2',...]
3233
# Default properties: name, creation
3334
# If get_mounts=True, mountpoint and mounted are also retrieved automatically
3435
# unlocking some functionality
35-
# To see all available properties use: % zfs list -o foo
36+
# To see all available properties use: % zfs list -o (-or-) % zpool list -o
3637
poolset = conn.load_poolset()
3738
3839
# Load a pool by name
@@ -101,6 +102,12 @@ See examples folder
101102
# (dt_from --> dt_to) | (dt_from --> dt_from + tdelta) | (dt_to - tdelta --> dt_to) | (dt_from --> now)
102103
```
103104

105+
### `<Dataset>.get_property(str)`
106+
```
107+
# get_property(str) - Return zfs item or zpool property
108+
# - use zfs_props or zpool_props to grab non-defaulted properties
109+
```
110+
104111
### `<Dataset>.get_diffs()`
105112
```
106113
# get_diffs() - Gets Diffs in snapshot or between snapshots (if snap_to is specified)

examples/ex_local.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def main(argv):
3333

3434

3535
# Find Snapshots of name autosnap* in the last 4 hours
36-
snapshots = ds.find_snapshots({'name': 'autosnap*', 'tdelta': '4H'})
36+
snapshots = ds.find_snapshots({'name': 'autosnap*', 'tdelta': '4h'})
3737

3838

3939
# Iterate through all pools and print all datasets

examples/ex_other.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ def main(argv):
1515
conn = zfs.Connection(host='localhost')
1616

1717
# Load poolset
18-
# Note: the properties name and creation are automatically retrieved
18+
# Note: the zfs_props name and creation are automatically retrieved
1919
# If get_mounts=True, mountpoint and mounted are also retrieved automatically
20-
# To see all available properties use: % zfs list -o foo
21-
poolset = conn.load_poolset(get_mounts=True, properties=["avail", "usedsnap", "usedrefreserv", "usedchild"])
20+
# To see all available zfs_props use: % zfs list -o foo
21+
poolset = conn.load_poolset(get_mounts=True, zfs_props=["avail", "usedsnap", "usedrefreserv", "usedchild"])
2222

2323

2424
# Print all datasets test

examples/zfslib_ex_common.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88

99
def print_all_datasets(pool):
1010
allds = pool.get_all_datasets(with_depth=True)
11-
for (depth, ds) in allds:
12-
print("{}: {} {} ({})".format(pool.name, ' .'*depth, ds.name, ds.dspath))
13-
14-
15-
16-
def print_all_datasets(pool):
17-
allds = pool.get_all_datasets(with_depth=True)
11+
print(f"[Pool - {pool.name} ({pool.get_property('health')}, Frag: {pool.get_property('fragmentation')})]")
1812
for (depth, ds) in allds:
1913
print("{}: {} {} ({})".format(pool.name, ' .'*depth, ds.name, ds.dspath))
2014

other/props.ods

22.7 KB
Binary file not shown.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "zfslib"
3-
version = "0.9.3"
3+
version = "0.10.0"
44
description = "ZFS Utilities For Python3"
55
license = "MIT"
66
authors = ["Timothy C. Quinn"]

src/zfslib/zfslib.py

Lines changed: 91 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
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

1013
import subprocess
@@ -18,6 +21,8 @@
1821

1922
is_py2=(sys.version_info[0] == 2)
2023

24+
class __DEFAULT__(object):pass
25+
2126
class 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

7067
class 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

Comments
 (0)