1219089Spjd#! /usr/bin/python2.6
2209962Smm#
3209962Smm# CDDL HEADER START
4209962Smm#
5209962Smm# The contents of this file are subject to the terms of the
6209962Smm# Common Development and Distribution License (the "License").
7209962Smm# You may not use this file except in compliance with the License.
8209962Smm#
9209962Smm# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10209962Smm# or http://www.opensolaris.org/os/licensing.
11209962Smm# See the License for the specific language governing permissions
12209962Smm# and limitations under the License.
13209962Smm#
14209962Smm# When distributing Covered Code, include this CDDL HEADER in each
15209962Smm# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16209962Smm# If applicable, add the following below this CDDL HEADER, with the
17209962Smm# fields enclosed by brackets "[]" replaced with your own identifying
18209962Smm# information: Portions Copyright [yyyy] [name of copyright owner]
19209962Smm#
20209962Smm# CDDL HEADER END
21209962Smm#
22219089Spjd# Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
23209962Smm#
24209962Smm
25209962Smm"""This module implements the "zfs userspace" and "zfs groupspace" subcommands.
26209962SmmThe only public interface is the zfs.userspace.do_userspace() function."""
27209962Smm
28209962Smmimport optparse
29209962Smmimport sys
30209962Smmimport pwd
31209962Smmimport grp
32209962Smmimport errno
33219089Spjdimport solaris.misc
34219089Spjdimport zfs.util
35219089Spjdimport zfs.ioctl
36219089Spjdimport zfs.dataset
37219089Spjdimport zfs.table
38209962Smm
39209962Smm_ = zfs.util._
40209962Smm
41209962Smm# map from property name prefix -> (field name, isgroup)
42209962Smmprops = {
43209962Smm    "userused@": ("used", False),
44209962Smm    "userquota@": ("quota", False),
45209962Smm    "groupused@": ("used", True),
46209962Smm    "groupquota@": ("quota", True),
47209962Smm}
48209962Smm
49209962Smmdef skiptype(options, prop):
50209962Smm	"""Return True if this property (eg "userquota@") should be skipped."""
51209962Smm	(field, isgroup) = props[prop]
52209962Smm	if field not in options.fields:
53209962Smm		return True
54209962Smm	if isgroup and "posixgroup" not in options.types and \
55209962Smm	    "smbgroup" not in options.types:
56209962Smm		return True
57209962Smm	if not isgroup and "posixuser" not in options.types and \
58209962Smm	    "smbuser" not in options.types:
59209962Smm		return True
60209962Smm	return False
61209962Smm
62209962Smmdef new_entry(options, isgroup, domain, rid):
63209962Smm	"""Return a dict("field": value) for this domain (string) + rid (int)"""
64209962Smm
65209962Smm	if domain:
66209962Smm		idstr = "%s-%u" % (domain, rid)
67209962Smm	else:
68209962Smm		idstr = "%u" % rid
69209962Smm
70209962Smm	(typename, mapfunc) = {
71219089Spjd	    (1, 1): ("SMB Group",   lambda id: solaris.misc.sid_to_name(id, 0)),
72209962Smm	    (1, 0): ("POSIX Group", lambda id: grp.getgrgid(int(id)).gr_name),
73219089Spjd	    (0, 1): ("SMB User",    lambda id: solaris.misc.sid_to_name(id, 1)),
74209962Smm	    (0, 0): ("POSIX User",  lambda id: pwd.getpwuid(int(id)).pw_name)
75209962Smm	}[isgroup, bool(domain)]
76209962Smm
77209962Smm	if typename.lower().replace(" ", "") not in options.types:
78209962Smm		return None
79209962Smm
80209962Smm	v = dict()
81209962Smm	v["type"] = typename
82209962Smm
83209962Smm	# python's getpwuid/getgrgid is confused by ephemeral uids
84209962Smm	if not options.noname and rid < 1<<31:
85209962Smm		try:
86209962Smm			v["name"] = mapfunc(idstr)
87209962Smm		except KeyError:
88209962Smm			pass
89209962Smm
90209962Smm	if "name" not in v:
91209962Smm		v["name"] = idstr
92209962Smm		if not domain:
93209962Smm			# it's just a number, so pad it with spaces so
94209962Smm			# that it will sort numerically
95209962Smm			v["name.sort"] = "%20d" % rid
96209962Smm	# fill in default values
97209962Smm	v["used"] = "0"
98209962Smm	v["used.sort"] = 0
99209962Smm	v["quota"] = "none"
100209962Smm	v["quota.sort"] = 0
101209962Smm	return v
102209962Smm
103219089Spjddef process_one_raw(acct, options, prop, elem):
104219089Spjd	"""Update the acct dict to incorporate the
105209962Smm	information from this elem from Dataset.userspace(prop)."""
106209962Smm
107209962Smm	(domain, rid, value) = elem
108209962Smm	(field, isgroup) = props[prop]
109209962Smm
110209962Smm	if options.translate and domain:
111209962Smm		try:
112219089Spjd			rid = solaris.misc.sid_to_id("%s-%u" % (domain, rid),
113209962Smm			    not isgroup)
114209962Smm			domain = None
115209962Smm		except KeyError:
116209962Smm			pass;
117209962Smm	key = (isgroup, domain, rid)
118209962Smm
119209962Smm	try:
120209962Smm		v = acct[key]
121209962Smm	except KeyError:
122209962Smm		v = new_entry(options, isgroup, domain, rid)
123209962Smm		if not v:
124209962Smm			return
125209962Smm		acct[key] = v
126209962Smm
127209962Smm	# Add our value to an existing value, which may be present if
128209962Smm	# options.translate is set.
129209962Smm	value = v[field + ".sort"] = value + v[field + ".sort"]
130209962Smm
131209962Smm	if options.parsable:
132209962Smm		v[field] = str(value)
133209962Smm	else:
134209962Smm		v[field] = zfs.util.nicenum(value)
135209962Smm
136209962Smmdef do_userspace():
137209962Smm	"""Implements the "zfs userspace" and "zfs groupspace" subcommands."""
138209962Smm
139209962Smm	def usage(msg=None):
140209962Smm		parser.print_help()
141209962Smm		if msg:
142209962Smm			print
143209962Smm			parser.exit("zfs: error: " + msg)
144209962Smm		else:
145209962Smm			parser.exit()
146209962Smm
147209962Smm	if sys.argv[1] == "userspace":
148209962Smm		defaulttypes = "posixuser,smbuser"
149209962Smm	else:
150209962Smm		defaulttypes = "posixgroup,smbgroup"
151209962Smm
152209962Smm	fields = ("type", "name", "used", "quota")
153219089Spjd	rjustfields = ("used", "quota")
154209962Smm	types = ("all", "posixuser", "smbuser", "posixgroup", "smbgroup")
155209962Smm
156209962Smm	u = _("%s [-niHp] [-o field[,...]] [-sS field] ... \n") % sys.argv[1]
157209962Smm	u += _("    [-t type[,...]] <filesystem|snapshot>")
158209962Smm	parser = optparse.OptionParser(usage=u, prog="zfs")
159209962Smm
160209962Smm	parser.add_option("-n", action="store_true", dest="noname",
161209962Smm	    help=_("Print numeric ID instead of user/group name"))
162209962Smm	parser.add_option("-i", action="store_true", dest="translate",
163209962Smm	    help=_("translate SID to posix (possibly ephemeral) ID"))
164209962Smm	parser.add_option("-H", action="store_true", dest="noheaders",
165209962Smm	    help=_("no headers, tab delimited output"))
166209962Smm	parser.add_option("-p", action="store_true", dest="parsable",
167209962Smm	    help=_("exact (parsable) numeric output"))
168209962Smm	parser.add_option("-o", dest="fields", metavar="field[,...]",
169209962Smm	    default="type,name,used,quota",
170209962Smm	    help=_("print only these fields (eg type,name,used,quota)"))
171209962Smm	parser.add_option("-s", dest="sortfields", metavar="field",
172209962Smm	    type="choice", choices=fields, default=list(),
173209962Smm	    action="callback", callback=zfs.util.append_with_opt,
174209962Smm	    help=_("sort field"))
175209962Smm	parser.add_option("-S", dest="sortfields", metavar="field",
176209962Smm	    type="choice", choices=fields, #-s sets the default
177209962Smm	    action="callback", callback=zfs.util.append_with_opt,
178209962Smm	    help=_("reverse sort field"))
179209962Smm	parser.add_option("-t", dest="types", metavar="type[,...]",
180209962Smm	    default=defaulttypes,
181209962Smm	    help=_("print only these types (eg posixuser,smbuser,posixgroup,smbgroup,all)"))
182209962Smm
183209962Smm	(options, args) = parser.parse_args(sys.argv[2:])
184209962Smm	if len(args) != 1:
185209962Smm		usage(_("wrong number of arguments"))
186209962Smm	dsname = args[0]
187209962Smm
188209962Smm	options.fields = options.fields.split(",")
189209962Smm	for f in options.fields:
190209962Smm		if f not in fields:
191209962Smm			usage(_("invalid field %s") % f)
192209962Smm
193209962Smm	options.types = options.types.split(",")
194209962Smm	for t in options.types:
195209962Smm		if t not in types:
196209962Smm			usage(_("invalid type %s") % t)
197209962Smm
198209962Smm	if not options.sortfields:
199209962Smm		options.sortfields = [("-s", "type"), ("-s", "name")]
200209962Smm
201209962Smm	if "all" in options.types:
202209962Smm		options.types = types[1:]
203209962Smm
204209962Smm	ds = zfs.dataset.Dataset(dsname, types=("filesystem"))
205209962Smm
206219089Spjd	if ds.getprop("jailed") and solaris.misc.isglobalzone():
207209962Smm		options.noname = True
208209962Smm
209209962Smm	if not ds.getprop("useraccounting"):
210209962Smm		print(_("Initializing accounting information on old filesystem, please wait..."))
211209962Smm		ds.userspace_upgrade()
212209962Smm
213219089Spjd	# gather and process accounting information
214219089Spjd	# Due to -i, we need to keep a dict, so we can potentially add
215219089Spjd	# together the posix ID and SID's usage.  Grr.
216209962Smm	acct = dict()
217209962Smm	for prop in props.keys():
218209962Smm		if skiptype(options, prop):
219209962Smm			continue;
220209962Smm		for elem in ds.userspace(prop):
221219089Spjd			process_one_raw(acct, options, prop, elem)
222209962Smm
223209962Smm	def cmpkey(val):
224209962Smm		l = list()
225209962Smm		for (opt, field) in options.sortfields:
226209962Smm			try:
227209962Smm				n = val[field + ".sort"]
228209962Smm			except KeyError:
229209962Smm				n = val[field]
230209962Smm			if opt == "-S":
231209962Smm				# reverse sorting
232209962Smm				try:
233209962Smm					n = -n
234209962Smm				except TypeError:
235209962Smm					# it's a string; decompose it
236209962Smm					# into an array of integers,
237209962Smm					# each one the negative of that
238209962Smm					# character
239209962Smm					n = [-ord(c) for c in n]
240209962Smm			l.append(n)
241209962Smm		return l
242209962Smm
243219089Spjd	t = zfs.table.Table(options.fields, rjustfields)
244219089Spjd	for val in acct.itervalues():
245219089Spjd		t.addline(cmpkey(val), val)
246219089Spjd	t.printme(not options.noheaders)
247