xref: /illumos-gate/usr/src/lib/pyzfs/common/userspace.py (revision d67944fbe3fa0b31893a7116a09b0718eecf6078)
1#! /usr/bin/python2.4
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# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23# Use is subject to license terms.
24#
25
26"""This module implements the "zfs userspace" and "zfs groupspace" subcommands.
27The only public interface is the zfs.userspace.do_userspace() function."""
28
29import zfs.util
30import zfs.ioctl
31import zfs.dataset
32import optparse
33import sys
34import pwd
35import grp
36import errno
37
38_ = zfs.util._
39
40# map from property name prefix -> (field name, isgroup)
41props = {
42    "userused@": ("used", False),
43    "userquota@": ("quota", False),
44    "groupused@": ("used", True),
45    "groupquota@": ("quota", True),
46}
47
48def skiptype(options, prop):
49	"""Return True if this property (eg "userquota@") should be skipped."""
50	(field, isgroup) = props[prop]
51	if field not in options.fields:
52		return True
53	if isgroup and "posixgroup" not in options.types and \
54	    "smbgroup" not in options.types:
55		return True
56	if not isgroup and "posixuser" not in options.types and \
57	    "smbuser" not in options.types:
58		return True
59	return False
60
61def updatemax(d, k, v):
62	d[k] = max(d.get(k, None), v)
63
64def new_entry(options, isgroup, domain, rid):
65	"""Return a dict("field": value) for this domain (string) + rid (int)"""
66
67	if domain:
68		idstr = "%s-%u" % (domain, rid)
69	else:
70		idstr = "%u" % rid
71
72	(typename, mapfunc) = {
73	    (1, 1): ("SMB Group",   lambda id: zfs.ioctl.sid_to_name(id, 0)),
74	    (1, 0): ("POSIX Group", lambda id: grp.getgrgid(int(id)).gr_name),
75	    (0, 1): ("SMB User",    lambda id: zfs.ioctl.sid_to_name(id, 1)),
76	    (0, 0): ("POSIX User",  lambda id: pwd.getpwuid(int(id)).pw_name)
77	}[isgroup, bool(domain)]
78
79	if typename.lower().replace(" ", "") not in options.types:
80		return None
81
82	v = dict()
83	v["type"] = typename
84
85	# python's getpwuid/getgrgid is confused by ephemeral uids
86	if not options.noname and rid < 1<<31:
87		try:
88			v["name"] = mapfunc(idstr)
89		except KeyError:
90			pass
91
92	if "name" not in v:
93		v["name"] = idstr
94		if not domain:
95			# it's just a number, so pad it with spaces so
96			# that it will sort numerically
97			v["name.sort"] = "%20d" % rid
98	# fill in default values
99	v["used"] = "0"
100	v["used.sort"] = 0
101	v["quota"] = "none"
102	v["quota.sort"] = 0
103	return v
104
105def process_one_raw(acct, maxfieldlen, options, prop, elem):
106	"""Update the acct and maxfieldlen dicts to incorporate the
107	information from this elem from Dataset.userspace(prop)."""
108
109	(domain, rid, value) = elem
110	(field, isgroup) = props[prop]
111
112	if options.translate and domain:
113		try:
114			rid = zfs.ioctl.sid_to_id("%s-%u" % (domain, rid),
115			    not isgroup)
116			domain = None
117		except KeyError:
118			pass;
119	key = (isgroup, domain, rid)
120
121	try:
122		v = acct[key]
123	except KeyError:
124		v = new_entry(options, isgroup, domain, rid)
125		if not v:
126			return
127		acct[key] = v
128
129	# Add our value to an existing value, which may be present if
130	# options.translate is set.
131	value = v[field + ".sort"] = value + v[field + ".sort"]
132
133	if options.parsable:
134		v[field] = str(value)
135	else:
136		v[field] = zfs.util.nicenum(value)
137	for k in v.keys():
138		# some of the .sort fields are integers, so have no len()
139		if isinstance(v[k], str):
140			updatemax(maxfieldlen, k, len(v[k]))
141
142def do_userspace():
143	"""Implements the "zfs userspace" and "zfs groupspace" subcommands."""
144
145	def usage(msg=None):
146		parser.print_help()
147		if msg:
148			print
149			parser.exit("zfs: error: " + msg)
150		else:
151			parser.exit()
152
153	if sys.argv[1] == "userspace":
154		defaulttypes = "posixuser,smbuser"
155	else:
156		defaulttypes = "posixgroup,smbgroup"
157
158	fields = ("type", "name", "used", "quota")
159	ljustfields = ("type", "name")
160	types = ("all", "posixuser", "smbuser", "posixgroup", "smbgroup")
161
162	u = _("%s [-niHp] [-o field[,...]] [-sS field] ... \n") % sys.argv[1]
163	u += _("    [-t type[,...]] <filesystem|snapshot>")
164	parser = optparse.OptionParser(usage=u, prog="zfs")
165
166	parser.add_option("-n", action="store_true", dest="noname",
167	    help=_("Print numeric ID instead of user/group name"))
168	parser.add_option("-i", action="store_true", dest="translate",
169	    help=_("translate SID to posix (possibly ephemeral) ID"))
170	parser.add_option("-H", action="store_true", dest="noheaders",
171	    help=_("no headers, tab delimited output"))
172	parser.add_option("-p", action="store_true", dest="parsable",
173	    help=_("exact (parsable) numeric output"))
174	parser.add_option("-o", dest="fields", metavar="field[,...]",
175	    default="type,name,used,quota",
176	    help=_("print only these fields (eg type,name,used,quota)"))
177	parser.add_option("-s", dest="sortfields", metavar="field",
178	    type="choice", choices=fields, default=list(),
179	    action="callback", callback=zfs.util.append_with_opt,
180	    help=_("sort field"))
181	parser.add_option("-S", dest="sortfields", metavar="field",
182	    type="choice", choices=fields, #-s sets the default
183	    action="callback", callback=zfs.util.append_with_opt,
184	    help=_("reverse sort field"))
185	parser.add_option("-t", dest="types", metavar="type[,...]",
186	    default=defaulttypes,
187	    help=_("print only these types (eg posixuser,smbuser,posixgroup,smbgroup,all)"))
188
189	(options, args) = parser.parse_args(sys.argv[2:])
190	if len(args) != 1:
191		usage(_("wrong number of arguments"))
192	dsname = args[0]
193
194	options.fields = options.fields.split(",")
195	for f in options.fields:
196		if f not in fields:
197			usage(_("invalid field %s") % f)
198
199	options.types = options.types.split(",")
200	for t in options.types:
201		if t not in types:
202			usage(_("invalid type %s") % t)
203
204	if not options.sortfields:
205		options.sortfields = [("-s", "type"), ("-s", "name")]
206
207	if "all" in options.types:
208		options.types = types[1:]
209
210	ds = zfs.dataset.Dataset(dsname, types=("filesystem"))
211
212	if ds.getprop("zoned") and zfs.ioctl.isglobalzone():
213		options.noname = True
214
215	if not ds.getprop("useraccounting"):
216		print(_("Initializing accounting information on old filesystem, please wait..."))
217		ds.userspace_upgrade()
218
219	acct = dict()
220	maxfieldlen = dict()
221
222	# gather and process accounting information
223	for prop in props.keys():
224		if skiptype(options, prop):
225			continue;
226		for elem in ds.userspace(prop):
227			process_one_raw(acct, maxfieldlen, options, prop, elem)
228
229	# print out headers
230	if not options.noheaders:
231		line = str()
232		for field in options.fields:
233			# make sure the field header will fit
234			updatemax(maxfieldlen, field, len(field))
235
236			if field in ljustfields:
237				fmt = "%-*s  "
238			else:
239				fmt = "%*s  "
240			line += fmt % (maxfieldlen[field], field.upper())
241		print(line)
242
243	# custom sorting func
244	def cmpkey(val):
245		l = list()
246		for (opt, field) in options.sortfields:
247			try:
248				n = val[field + ".sort"]
249			except KeyError:
250				n = val[field]
251			if opt == "-S":
252				# reverse sorting
253				try:
254					n = -n
255				except TypeError:
256					# it's a string; decompose it
257					# into an array of integers,
258					# each one the negative of that
259					# character
260					n = [-ord(c) for c in n]
261			l.append(n)
262		return l
263
264	# print out data lines
265	for val in sorted(acct.itervalues(), key=cmpkey):
266		line = str()
267		for field in options.fields:
268			if options.noheaders:
269				line += val[field]
270				line += "\t"
271			else:
272				if field in ljustfields:
273					fmt = "%-*s  "
274				else:
275					fmt = "%*s  "
276				line += fmt % (maxfieldlen[field], val[field])
277		print(line)
278