Skip to content

Commit ac983e1

Browse files
committed
added getent.py
1 parent fb6f8ae commit ac983e1

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

getent.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#!/usr/bin/env python
2+
# vim:ts=4:sts=4:sw=4:et
3+
#
4+
# Author: Hari Sekhon
5+
# Date: 2016-11-20 12:35:49 +0000 (Sun, 20 Nov 2016)
6+
#
7+
# https://github.com/harisekhon/pytools
8+
#
9+
# License: see accompanying Hari Sekhon LICENSE file
10+
#
11+
# If you're using my code you're welcome to connect with me on LinkedIn
12+
# and optionally send me feedback to help steer this or other code I publish
13+
#
14+
# https://www.linkedin.com/in/harisekhon
15+
#
16+
17+
"""
18+
19+
Tool to abstract and normalize Mac's user/group/host system resolver calls to the same format
20+
as the more standard Linux getent for simplified scripting between the two platforms
21+
22+
Will detect if the platform is Mac and call the necessary commands and translate.
23+
24+
For Linux it becomes a straight pass through to getent.
25+
26+
Supported getent commands: passwd, group
27+
28+
Tested on Linux and Mac OSX 10.10.x (Yosemite)
29+
30+
"""
31+
32+
from __future__ import absolute_import
33+
from __future__ import division
34+
from __future__ import print_function
35+
#from __future__ import unicode_literals
36+
37+
import os
38+
import platform
39+
import subprocess
40+
import sys
41+
import time
42+
import traceback
43+
try:
44+
import psutil
45+
except ImportError:
46+
print(traceback.format_exc(), end='')
47+
sys.exit(4)
48+
srcdir = os.path.abspath(os.path.dirname(__file__))
49+
libdir = os.path.join(srcdir, 'pylib')
50+
sys.path.append(libdir)
51+
try:
52+
# pylint: disable=wrong-import-position
53+
from harisekhon.utils import log, die, qquit, plural, which, isList, isInt
54+
from harisekhon import CLI
55+
except ImportError as _:
56+
print('module import failed: %s' % _, file=sys.stderr)
57+
print("Did you remember to build the project by running 'make'?", file=sys.stderr)
58+
print("Alternatively perhaps you tried to copy this program out without it's adjacent libraries?", file=sys.stderr)
59+
sys.exit(4)
60+
61+
__author__ = 'Hari Sekhon'
62+
__version__ = '0.1'
63+
64+
65+
class Getent(CLI):
66+
67+
def __init__(self):
68+
# Python 2.x
69+
super(Getent, self).__init__()
70+
# Python 3.x
71+
# super().__init__()
72+
# special case to make all following args belong to the passed in command and not to this program
73+
#self._CLI__parser.disable_interspersed_args()
74+
self._CLI__parser.set_usage('{prog} [options] <command> <args> ...'.format(prog=self._prog))
75+
76+
def timeout_handler(self, signum, frame): # pylint: disable=unused-argument
77+
for child in psutil.Process().children():
78+
child.kill()
79+
time.sleep(1)
80+
qquit('UNKNOWN', 'self timed out after %d second%s' % (self.timeout, plural(self.timeout)))
81+
82+
def run(self):
83+
if not self.args:
84+
self.usage()
85+
command = self.args[0]
86+
if len(self.args) > 1:
87+
args = self.args[1:]
88+
else:
89+
args = []
90+
args_string = ' '.join(self.args[1:])
91+
operating_system = platform.system()
92+
if operating_system == 'Darwin':
93+
log.info('detected system as Mac')
94+
log.info('calling mac_getent_%s(%s)', command, args_string)
95+
(formatted_output, returncode) = self.mac_getent(command, args)
96+
if formatted_output:
97+
print(formatted_output)
98+
sys.exit(returncode)
99+
elif operating_system == 'Linux':
100+
log.info('detected system as Linux')
101+
log.info('calling %s %s', command, args_string)
102+
sys.exit(subprocess.call(command, args, shell=False))
103+
else:
104+
die("operating system '{operating_system}' is not one of the supported Linux or Darwin (Mac)"\
105+
.format(operating_system=operating_system))
106+
107+
def mac_getent(self, command, args):
108+
# if command == 'passwd':
109+
# (output, returncode) = self.cmd(command, args)
110+
# formatted_output = self.mac_getent_passwd(args)
111+
# elif command == 'group':
112+
# (output, returncode) = self.cmd(command, args)
113+
# formatted_output = self.mac_getent_group(output)
114+
# elif command == 'host':
115+
# (output, returncode) = self.cmd(command, args)
116+
# formatted_output = self.mac_getent_host(output)
117+
if command in ('passwd', 'group'): #, 'host'):
118+
# might be too clever to dynamically determine the method to reduce code dup
119+
(formatted_output, returncode) = getattr(self, 'mac_getent_{command}'.format(command=command))(args)
120+
else:
121+
die("unsupported getent command '{0}', must be one of: passwd, group, host".format(command))
122+
return (formatted_output, returncode)
123+
124+
def mac_getent_passwd(self, args):
125+
arg = self.mac_get_arg(args)
126+
final_returncode = 0
127+
final_output = ""
128+
if arg:
129+
(final_output, final_returncode) = self.mac_getent_passwd_user(arg)
130+
else:
131+
users = [user for user in
132+
subprocess.Popen('dscl . -list /Users'.split(),
133+
stdout=subprocess.PIPE).stdout.read().split('\n')
134+
if user]
135+
log.info('found users: %s', users)
136+
for user in users:
137+
(formatted_output, returncode) = self.mac_getent_passwd_user(user)
138+
if formatted_output:
139+
final_output += formatted_output + '\n'
140+
if returncode > final_returncode:
141+
final_returncode = returncode
142+
final_output = final_output.rstrip('\n')
143+
# reorder output by UID to be similar to what you'd see on Linux
144+
lines = final_output.split('\n')
145+
final_output = '\n'.join(sorted(lines, cmp=lambda x, y: cmp(int(x.split(':')[2]), int(y.split(':')[2]))))
146+
return (final_output, final_returncode)
147+
148+
def mac_getent_passwd_user(self, user):
149+
log.info('mac_getent_passwd_user(%s)', user)
150+
command = 'dscl . -read /Users/{user}'.format(user=user)
151+
(output, returncode) = self.cmd(command)
152+
user = password = uid = gid = name = homedir = shell = ''
153+
#log.info('parsing output for passwd conversion')
154+
for line in output.split('\n'):
155+
tokens = line.split()
156+
if len(tokens) < 2:
157+
continue
158+
field = tokens[0]
159+
value = tokens[1]
160+
if field == 'RecordName:':
161+
user = value
162+
elif field == 'Password:':
163+
password = value
164+
elif field == 'UniqueID:':
165+
uid = value
166+
elif field == 'PrimaryGroupID:':
167+
gid = value
168+
elif field == 'RealName:':
169+
name = value
170+
elif field == 'NFSHomeDirectory:':
171+
homedir = value
172+
elif field == 'UserShell:':
173+
shell = value
174+
if not user:
175+
return('', returncode)
176+
getent_record = '{user}:{password}:{uid}:{gid}:{name}:{homedir}:{shell}'.format\
177+
(user=user, password=password, uid=uid, gid=gid, name=name, homedir=homedir, shell=shell)
178+
if not isInt(uid, allow_negative=True):
179+
die("parsing error: UID '{uid}' is not numeric in record {record}!".format(uid=uid, record=getent_record))
180+
if not isInt(gid, allow_negative=True):
181+
die("parsing error: GID '{gid}' is not numeric in record {record}!".format(gid=gid, record=getent_record))
182+
return (getent_record, returncode)
183+
184+
def mac_getent_group(self, args):
185+
arg = self.mac_get_arg(args)
186+
final_returncode = 0
187+
final_output = ""
188+
if arg:
189+
(final_output, final_returncode) = self.mac_getent_group_name(arg)
190+
else:
191+
groups = [group for group in
192+
subprocess.Popen('dscl . -list /Groups'.split(),
193+
stdout=subprocess.PIPE).stdout.read().split('\n')
194+
if group]
195+
log.info('found groups: %s', groups)
196+
for group in groups:
197+
(formatted_output, returncode) = self.mac_getent_group_name(group)
198+
if formatted_output:
199+
final_output += formatted_output + '\n'
200+
if returncode > final_returncode:
201+
final_returncode = returncode
202+
final_output = final_output.rstrip('\n')
203+
# reorder output by GID to be similar to what you'd see on Linux
204+
lines = final_output.split('\n')
205+
final_output = '\n'.join(sorted(lines, cmp=lambda x, y: cmp(int(x.split(':')[0]), int(y.split(':')[0]))))
206+
return (final_output, final_returncode)
207+
208+
def mac_getent_group_name(self, group):
209+
log.info('mac_getent_group_name(%s)', group)
210+
command = 'dscl . -read /Groups/{group}'.format(group=group)
211+
(output, returncode) = self.cmd(command)
212+
gid = password = name = members = ''
213+
#log.info('parsing output for group conversion')
214+
output = output.split('\n')
215+
for index, line in enumerate(output):
216+
tokens = line.split()
217+
if len(tokens) < 1:
218+
continue
219+
field = tokens[0]
220+
if len(tokens) < 2:
221+
value = ''
222+
else:
223+
value = tokens[1]
224+
if field == 'PrimaryGroupID:':
225+
gid = value
226+
elif field == 'Password:':
227+
password = value
228+
elif field == 'RealName:':
229+
name = value
230+
if not value and len(output) > index + 1 and output[index+1].startswith(' '):
231+
name = output[index+1].strip()
232+
elif not name and field == 'RecordName:':
233+
name = value
234+
elif field == 'GroupMembership:':
235+
members = ','.join(tokens[1:])
236+
if not gid:
237+
return('', returncode)
238+
getent_record = '{gid}:{password}:{name}:{members}'.format\
239+
(gid=gid, password=password, name=name, members=members)
240+
if not isInt(gid, allow_negative=True):
241+
die("parsing error: GID '{gid}' is not numeric in record {record}!".format(gid=gid, record=getent_record))
242+
return (getent_record, returncode)
243+
244+
# TODO: dscacheutil is much too slow to be used as a replacement for doing `host <fqdn>`, figure out faster way
245+
# dscacheutil -q host -a name <fqdn>
246+
# def mac_getent_host(self, args):
247+
# arg = self.mac_get_arg(args)
248+
# (output, returncode) = self.cmd(command)
249+
250+
@staticmethod
251+
def mac_get_arg(args):
252+
if not args:
253+
return ''
254+
if not isList(args):
255+
die("non-list '{args}' passed to mac_getent_passwd()".format(args=args))
256+
if len(args) > 1:
257+
die('only one arg is supported on Mac at this time')
258+
arg = args[0]
259+
return arg
260+
261+
# TODO: make a slightly more generic version similar to Perl lib and put in PyLib
262+
@staticmethod
263+
def cmd(command):
264+
log.debug('command: %s', command)
265+
command_binary = command.split()[0]
266+
if not which(command_binary):
267+
die("command '{command}' not found in $PATH".format(command=command_binary))
268+
try:
269+
process = subprocess.Popen(command.split(),
270+
stdin=subprocess.PIPE,
271+
stdout=subprocess.PIPE,
272+
stderr=subprocess.STDOUT)
273+
(stdout, _) = process.communicate()
274+
process.wait()
275+
log.debug('returncode: %s', process.returncode)
276+
log.debug('output: %s\n', stdout)
277+
return(stdout, process.returncode)
278+
except subprocess.CalledProcessError as _:
279+
log.debug('CalledProcessError Exception!')
280+
log.debug('returncode: %s', _.returncode)
281+
log.debug('output: %s\n', _.output)
282+
return(_.output, _.returncode)
283+
284+
285+
if __name__ == '__main__':
286+
Getent().main()

0 commit comments

Comments
 (0)