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