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