dsadm.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. #! /usr/bin/python2
  2. # Authors:
  3. # Thierry Bordaz <[email protected]>
  4. #
  5. # Copyright (C) 2015 Red Hat
  6. # see file 'COPYING' for use and warranty information
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. import sys
  21. import os
  22. import argparse
  23. import pdb
  24. import tempfile
  25. import time
  26. import pwd
  27. import grp
  28. import platform
  29. import socket
  30. import shutil
  31. from subprocess import Popen, PIPE, STDOUT
  32. import string
  33. SETUP_DS = "/sbin/setup-ds.pl"
  34. REMOVE_DS = "/sbin/remove-ds.pl"
  35. INITCONFIGDIR = ".dirsrv"
  36. SCRIPT_START = "start-slapd"
  37. SCRIPT_STOP = "stop-slapd"
  38. SCRIPT_RESTART = "restart-slapd"
  39. ENVIRON_SERVERID = '389-SERVER-ID'
  40. ENVIRON_USER = '389-USER'
  41. ENVIRON_GROUP = '389-GROUP'
  42. ENVIRON_DIRECTORY = '389-DIRECTORY'
  43. ENVIRON_PORT = '389-PORT'
  44. ENVIRON_SECURE_PORT = '389-SECURE-PORT'
  45. DEFAULT_PORT_ROOT = str(389)
  46. DEFAULT_PORT_NON_ROOT = str(1389)
  47. DEFAULT_SECURE_PORT_ROOT = str(636)
  48. DEFAULT_SECURE_PORT_NON_ROOT = str(1636)
  49. DEFAULT_USER = 'nobody'
  50. DEFAULT_GROUP = 'nobody'
  51. DEFAULT_ROOT_DN = 'cn=Directory Manager'
  52. DEFAULT_HOSTNAME = socket.gethostname()
  53. def validate_user(user):
  54. '''
  55. If a user is provided it returns its username
  56. else it returns the current username.
  57. It checks that the userId or userName exists
  58. :param: user (optional) can be a userName or userId
  59. :return: userName of the provided user, if none is provided, it returns current user name
  60. '''
  61. assert(user)
  62. if user.isdigit():
  63. try:
  64. username = pwd.getpwuid(int(user)).pw_name
  65. except KeyError:
  66. raise KeyError('Unknown userId %d' % user)
  67. return username
  68. else:
  69. try:
  70. pwd.getpwnam(user).pw_uid
  71. except KeyError:
  72. raise KeyError('Unknown userName %s' % user)
  73. return user
  74. def get_default_user():
  75. user = os.environ.get(ENVIRON_USER, None)
  76. if not user:
  77. user = os.getuid()
  78. return str(user)
  79. def get_default_group():
  80. '''
  81. If a group is provided it returns its groupname
  82. else it returns the current groupname.
  83. It checks that the groupId or groupName exists
  84. :param: group (optional) can be a groupName or groupId
  85. :return: groupName of the provided group, if none is provided, it returns current group name
  86. '''
  87. group = os.environ.get(ENVIRON_GROUP, None)
  88. if not group:
  89. return pwd.getpwuid(os.getuid()).pw_name
  90. return group
  91. def validate_group(group):
  92. assert(group)
  93. if str(group).isdigit():
  94. try:
  95. groupname = grp.getgrgid(group).gr_name
  96. return groupname
  97. except:
  98. raise KeyError('Unknown groupId %d' % group)
  99. else:
  100. try:
  101. groupname = grp.getgrnam(group).gr_name
  102. return groupname
  103. except:
  104. raise KeyError('Unknown groupName %s' % group)
  105. def test_get_group():
  106. try:
  107. grpname = get_default_group()
  108. print 'get_group: %s' % grpname
  109. except:
  110. raise
  111. print "Can not find user group"
  112. pass
  113. try:
  114. grpname = get_default_group(group='tbordaz')
  115. print 'get_group: %s' % grpname
  116. except:
  117. raise
  118. print "Can not find user group"
  119. pass
  120. try:
  121. grpname = get_default_group(group='coucou')
  122. print 'get_group: %s' % grpname
  123. except:
  124. print "Can not find user group coucou"
  125. pass
  126. try:
  127. grpname = get_default_group('thierry')
  128. print 'get_group: %s' % grpname
  129. except:
  130. raise
  131. print "Can not find user group thierry"
  132. pass
  133. try:
  134. grpname = get_default_group(1000)
  135. print 'get_group: %s' % grpname
  136. except:
  137. raise
  138. print "Can not find user group 1000"
  139. pass
  140. try:
  141. grpname = get_default_group(20532)
  142. print 'get_group: %s' % grpname
  143. except:
  144. raise
  145. print "Can not find user group 20532"
  146. pass
  147. try:
  148. grpname = get_default_group(123)
  149. print 'get_group: %s' % grpname
  150. except:
  151. print "Can not find user group 123"
  152. pass
  153. def get_default_port():
  154. port = os.environ.get(ENVIRON_PORT, None)
  155. if port:
  156. return port
  157. if os.getuid() == 0:
  158. return DEFAULT_PORT_ROOT
  159. else:
  160. return DEFAULT_PORT_NON_ROOT
  161. def validate_port(port):
  162. assert port
  163. if not port.isdigit() or int(port) <= 0 :
  164. raise Exception("port number is invalid: %s" % port)
  165. def get_default_directory():
  166. directory = os.environ.get(ENVIRON_DIRECTORY, None)
  167. if not directory:
  168. directory = os.getcwd()
  169. return directory
  170. def validate_directory(directory):
  171. assert directory
  172. if not os.path.isdir(directory):
  173. raise Exception("Supplied directory path is not a directory")
  174. if not os.access(directory, os.W_OK):
  175. raise Exception("Supplied directory is not writable")
  176. def get_default_serverid():
  177. serverid = os.environ.get(ENVIRON_SERVERID, None)
  178. if not serverid:
  179. serverid = socket.gethostname().split('.')[0]
  180. return serverid
  181. def validate_serverid(serverid):
  182. if not serverid:
  183. raise Exception("Server id is not defined")
  184. return serverid
  185. def get_inst_dir(serverid):
  186. assert serverid
  187. home = os.getenv("HOME")
  188. inst_initconfig_file = "%s/%s/dirsrv-%s" % (home, INITCONFIGDIR, serverid)
  189. if not os.path.isfile(inst_initconfig_file):
  190. raise Exception("%s config file not found" % inst_initconfig_file)
  191. f = open(inst_initconfig_file, "r")
  192. for line in f:
  193. if line.startswith("INST_DIR"):
  194. inst_dir = line.split("=")[1]
  195. inst_dir = inst_dir.replace("\r", "")
  196. inst_dir = inst_dir.replace("\n", "")
  197. return inst_dir
  198. def sanity_check():
  199. if os.getuid() == 0:
  200. raise Exception("Not tested for root user.. sorry")
  201. home = os.getenv("HOME")
  202. inst_initconfig_dir = "%s/%s" % (home, INITCONFIGDIR)
  203. if not os.path.isdir(inst_initconfig_dir):
  204. raise Exception("Please create the directory \'%s\' and retry." % inst_initconfig_dir )
  205. class DSadmCmd(object):
  206. def __init__(self):
  207. self.version = '0.1'
  208. def _start_subparser(self, subparsers):
  209. start_parser = subparsers.add_parser(
  210. 'start',
  211. help='Start a Directory Server Instance')
  212. start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?',
  213. metavar='SERVER-ID',
  214. help='Server Identifier (Default: %s) ' % get_default_serverid())
  215. start_parser.set_defaults(func=self.start_action)
  216. def _stop_subparser(self, subparsers):
  217. start_parser = subparsers.add_parser(
  218. 'stop',
  219. help='Stop a Directory Server Instance')
  220. start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?',
  221. metavar='SERVER-ID',
  222. help='Server Identifier (Default: %s) ' % get_default_serverid())
  223. start_parser.set_defaults(func=self.stop_action)
  224. def _restart_subparser(self, subparsers):
  225. start_parser = subparsers.add_parser(
  226. 'restart',
  227. help='Retart a Directory Server Instance')
  228. start_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?',
  229. metavar='SERVER-ID',
  230. help='Server Identifier (Default: %s) ' % get_default_serverid())
  231. start_parser.set_defaults(func=self.restart_action)
  232. def _delete_subparser(self, subparsers):
  233. delete_parser = subparsers.add_parser(
  234. 'delete',
  235. help='Delete a Directory Server Instance')
  236. delete_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?',
  237. metavar='SERVER-ID',
  238. help='Server Identifier (Default: %s) ' % get_default_serverid())
  239. delete_parser.add_argument('-debug', '--debug', dest='debug_level', type=int, nargs='?',
  240. metavar='DEBUG_LEVEL',
  241. help='Debug level (Default: 0)')
  242. delete_parser.set_defaults(func=self.delete_action)
  243. def _create_subparser(self, subparsers):
  244. create_parser = subparsers.add_parser(
  245. 'create',
  246. help='Create a Directory Server Instance')
  247. create_parser.add_argument('-I', '--server-id', dest='server_id', type=str, nargs='?',
  248. metavar='SERVER-ID',
  249. help='Server Identifier (Default: %s) ' % get_default_serverid())
  250. create_parser.add_argument('-s', '--suffix', dest='suffix', type=str, nargs='?',
  251. metavar='SUFFIX-DN',
  252. help='Suffix (Default: create no suffix)')
  253. create_parser.add_argument('-p', '--port', dest='port', type=int, nargs='?',
  254. metavar='NON-SECURE-PORT',
  255. help='Normal Port to listen (Default: %s(root)/%s(non-root)) ' % (DEFAULT_PORT_ROOT, DEFAULT_PORT_NON_ROOT))
  256. create_parser.add_argument('-P', '--secure-port', dest='secure_port', type=int, nargs='?',
  257. metavar='SECURE-PORT',
  258. help='Secure Port to listen (Default: %s(root)/%s(non-root))' % (DEFAULT_SECURE_PORT_ROOT, DEFAULT_SECURE_PORT_NON_ROOT))
  259. create_parser.add_argument('-D', '--rootDN', dest='root_dn', type=str, nargs='?',
  260. metavar='ROOT-DN',
  261. help='Uses DN as Directory Manager DN (Default: \'%s\')' % (DEFAULT_ROOT_DN))
  262. create_parser.add_argument('-u', '--user-name', dest='user_name', type=str, nargs='?',
  263. metavar='USER-NAME',
  264. help='User name of the instance owner (Default: %s)' % DEFAULT_USER)
  265. create_parser.add_argument('-g', '--group-name', dest='group_name', type=str, nargs='?',
  266. metavar='GROUP-NAME',
  267. help='Group name of the instance owner (Default: %s)' % DEFAULT_GROUP)
  268. create_parser.add_argument('-d', '--directory-path', dest='directory_path', type=str, nargs='?',
  269. metavar='DIRECTORY-PATH',
  270. help='Installation directory path (Default: %s)' % get_default_directory())
  271. create_parser.add_argument('-debug', '--debug', dest='debug_level', type=int, nargs='?',
  272. metavar='DEBUG_LEVEL',
  273. help='Debug level (Default: 0)')
  274. create_parser.add_argument('-k', '--keep_template', dest='keep_template', type=str, nargs='?',
  275. help='Keep template file')
  276. create_parser.set_defaults(func=self.create_action)
  277. #
  278. # common function for start/stop/restart actions
  279. #
  280. def script_action(self, args, script, action_str):
  281. args = vars(args)
  282. serverid = args.get('server_id', None)
  283. if not serverid:
  284. serverid = get_default_serverid()
  285. script_file = "%s/%s" % (get_inst_dir(serverid), script)
  286. if not os.path.isfile(script_file):
  287. raise Exception("%s not found" % script_file)
  288. if not os.access(script_file, os.X_OK):
  289. raise Exception("%s not executable" % script_file)
  290. env = os.environ.copy()
  291. prog = [ script_file ]
  292. pipe = Popen(prog, cwd=os.getcwd(), env=env,
  293. stdin=PIPE, stdout=PIPE, stderr=STDOUT)
  294. child_stdin = pipe.stdin
  295. child_stdout = pipe.stdout
  296. for line in child_stdout:
  297. sys.stdout.write(line)
  298. child_stdout.close()
  299. child_stdin.close()
  300. rc = pipe.wait()
  301. if rc == 0:
  302. print "Directory %s %s" % (serverid, action_str)
  303. else:
  304. print "Failure: directory %s not %s (%s)" % (serverid, action_str, rc)
  305. return
  306. def start_action(self, args):
  307. self.script_action(args, SCRIPT_START, "started")
  308. def stop_action(self, args):
  309. self.script_action(args, SCRIPT_STOP, "stopped")
  310. def restart_action(self, args):
  311. self.script_action(args, SCRIPT_RESTART, "restarted")
  312. def delete_action(self, args):
  313. args = vars(args)
  314. serverid = args.get('server_id', None)
  315. if not serverid:
  316. serverid = get_default_serverid()
  317. #prepare the remove-ds options
  318. debug_level = args.get('debug_level', None)
  319. if debug_level:
  320. debug_str = ['-d']
  321. for i in range(1, int(debug_level)):
  322. debug_str.append('d')
  323. debug_str = ''.join(debug_str)
  324. env = os.environ.copy()
  325. prog = [REMOVE_DS]
  326. if debug_level:
  327. prog.append(debug_str)
  328. prog.append("-i")
  329. prog.append("slapd-%s" % serverid)
  330. # run the REMOVE_DS command and print the possible output
  331. pipe = Popen(prog, cwd=os.getcwd(), env=env,
  332. stdin=PIPE, stdout=PIPE, stderr=STDOUT)
  333. child_stdin = pipe.stdin
  334. child_stdout = pipe.stdout
  335. for line in child_stdout:
  336. if debug_level:
  337. sys.stdout.write(line)
  338. child_stdout.close()
  339. child_stdin.close()
  340. rc = pipe.wait()
  341. if rc == 0:
  342. print "Directory server \'%s\' successfully deleted" % serverid
  343. else:
  344. print "Fail to delete directory \'%s\': %d" % (serverid, rc)
  345. return
  346. #
  347. # used by create subcommand to build the template file
  348. #
  349. def _create_setup_ds_file(self, args, user=None, group=None):
  350. # Get/checks the argument with the following order
  351. # - parameter
  352. # - Environment
  353. # - default
  354. serverid = args.get('server_id', None)
  355. if not serverid:
  356. serverid = get_default_serverid()
  357. serverid = validate_serverid(serverid)
  358. username = args.get('user_name', None)
  359. if not username:
  360. username = get_default_user()
  361. username = validate_user(username)
  362. groupname = args.get('group_name', None)
  363. if not groupname:
  364. groupname = get_default_group()
  365. groupname = validate_group(groupname)
  366. directoryname = args.get('directory_path', None)
  367. if not directoryname:
  368. directoryname = get_default_directory()
  369. validate_directory(directoryname)
  370. portnumber = args.get('port', None)
  371. if not portnumber:
  372. portnumber = get_default_port()
  373. validate_port(portnumber)
  374. suffix = args.get('suffix', None)
  375. tempf = tempfile.NamedTemporaryFile(delete=False)
  376. tempf.write('[General]\n')
  377. tempf.write('FullMachineName=%s\n' % DEFAULT_HOSTNAME)
  378. tempf.write('SuiteSpotUserID=%s\n' % username)
  379. tempf.write('SuiteSpotGroup=%s\n' % groupname)
  380. tempf.write('ServerRoot=%s\n' % directoryname)
  381. tempf.write('\n')
  382. tempf.write('[slapd]\n')
  383. tempf.write('ServerPort=1389\n')
  384. tempf.write('ServerIdentifier=%s\n' % serverid)
  385. if suffix:
  386. tempf.write('Suffix=%s\n' % suffix)
  387. tempf.write('RootDN=cn=Directory Manager\n')
  388. tempf.write('RootDNPwd=Secret12\n')
  389. tempf.write('sysconfdir=%s/etc\n' % directoryname)
  390. tempf.write('localstatedir=%s/var\n' % directoryname)
  391. tempf.write('inst_dir=%s/lib/dirsrv/slapd-%s\n'% (directoryname, serverid))
  392. tempf.write('config_dir=%s/etc/dirsrv/slapd-%s\n' % (directoryname, serverid))
  393. tempf.close()
  394. keep_template = args.get('keep_template', None)
  395. if keep_template:
  396. shutil.copy(tempf.name, keep_template)
  397. return tempf
  398. #
  399. # It silently creates an instance.
  400. # After creation the instance is started
  401. #
  402. def create_action(self, args):
  403. args = vars(args)
  404. # retrieve the serverid here just to log the final status
  405. serverid = args.get('server_id', None)
  406. if not serverid:
  407. serverid = get_default_serverid()
  408. # prepare the template file
  409. tempf = self._create_setup_ds_file(args)
  410. #prepare the setup-ds options
  411. debug_level = args.get('debug_level', None)
  412. if debug_level:
  413. debug_str = ['-d']
  414. for i in range(1, int(debug_level)):
  415. debug_str.append('d')
  416. debug_str = ''.join(debug_str)
  417. #
  418. # run the SETUP_DS command and print the possible output
  419. #
  420. env = os.environ.copy()
  421. prog = [SETUP_DS]
  422. if debug_level:
  423. prog.append(debug_str)
  424. prog.append("--silent")
  425. prog.append("--file=%s" % tempf.name)
  426. tempf.close()
  427. pipe = Popen(prog, cwd=os.getcwd(), env=env,
  428. stdin=PIPE, stdout=PIPE, stderr=STDOUT)
  429. child_stdin = pipe.stdin
  430. child_stdout = pipe.stdout
  431. for line in child_stdout:
  432. if debug_level:
  433. sys.stdout.write(line)
  434. child_stdout.close()
  435. child_stdin.close()
  436. os.unlink(tempf.name)
  437. rc = pipe.wait()
  438. if rc == 0:
  439. print "Directory server \'%s\' successfully created" % serverid
  440. else:
  441. print "Fail to create directory \'%s\': %d" % (serverid, rc)
  442. return
  443. #
  444. # parser of the main command. It contains subcommands
  445. #
  446. def get_parser(self, argv):
  447. parser = argparse.ArgumentParser(
  448. description='Managing a local directory server instance')
  449. subparsers = parser.add_subparsers(
  450. metavar='SUBCOMMAND',
  451. help='The action to perform')
  452. #pdb.set_trace()
  453. # subcommands
  454. self._create_subparser(subparsers)
  455. self._delete_subparser(subparsers)
  456. self._start_subparser(subparsers)
  457. self._stop_subparser(subparsers)
  458. self._restart_subparser(subparsers)
  459. # Sanity check that the debug level is valid
  460. args = vars(parser.parse_args(argv))
  461. debug_level = args.get('debug_level', None)
  462. if debug_level and (int(debug_level) < 1 or int(debug_level > 5)):
  463. raise Exception("invalid debug level: range 1..5")
  464. return parser
  465. def main(self, argv):
  466. sanity_check()
  467. parser = self.get_parser(argv)
  468. args = parser.parse_args(argv)
  469. args.func(args)
  470. return
  471. if __name__ == '__main__':
  472. DSadmCmd().main(sys.argv[1:])