PageRenderTime 54ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/commands.py

http://google-apps-for-your-domain-ldap-sync.googlecode.com/
Python | 1203 lines | 1167 code | 7 blank | 29 comment | 0 complexity | 70f7b1a6c4a7a9439a4e92d6a63a5b1a MD5 | raw file
  1. #!/usr/bin/python2.4
  2. #
  3. # Copyright 2006 Google, Inc.
  4. # All Rights Reserved
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License")
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. # This package requires:
  18. # 1) Python 2.4 or above,
  19. # 2) the "python-ldap" package,
  20. # which can be downloaded from http://python-ldap.sourceforge.net/
  21. # 3) Google Apps for Your Domain Provisioning API, version 1, Python
  22. # bindings.
  23. """ Command-line module for the LDAP Sync Tool
  24. class Commands: main class, subclass of the Python cmd.Cmd class
  25. """
  26. import cmd
  27. import last_update_time
  28. import logging
  29. import messages
  30. import os
  31. from google.appsforyourdomain import provisioning
  32. import pprint
  33. import time
  34. import userdb
  35. import utils
  36. pp = pprint.PrettyPrinter(indent=2)
  37. # the most user records to be displayed at a time
  38. MAX_USER_DISPLAY = 32
  39. class Commands(cmd.Cmd):
  40. """ main class, subclass of the Python cmd.Cmd class.
  41. Most of the methods in this module are either "do_<command>"
  42. or "help_<command", which is the cmd.Cmd convention for the
  43. <command> method or the <command> help. The commands are organized
  44. into groups, and within each group in alphabetical order by
  45. <command>. The groups are:
  46. 1) constructors & overhead
  47. 2) Configuration variable-setting
  48. 3) LDAP connections
  49. 4) LDAP searching and checking
  50. 5) Viewing the users
  51. 6) Managing attributes and mappings
  52. 7) Synchronizing with Google
  53. 8) Reading and writing the users to a file
  54. 9) Miscellaneous commands (batch & stop)
  55. 10) Utility methods
  56. """
  57. def __init__(self, ldap_context, users, google, config):
  58. """ Constructor.
  59. Args:
  60. ldap_context: LDAPCtxt object
  61. users: Users object
  62. google: sync_google object
  63. config: utils.Config object
  64. """
  65. cmd.Cmd.__init__(self)
  66. self.ldap_context = ldap_context
  67. self.users = users
  68. self.sync_google = google
  69. self._config = config
  70. self.new_users = None
  71. self.last_update = None
  72. # stuff needed by the Cmd superclass:
  73. self.completekey = None
  74. self.cmdqueue = []
  75. self.prompt = "Command: "
  76. self.prompt = messages.msg(messages.CMD_PROMPT)
  77. # code only for use of the unittest:
  78. self._testing_last_update = None
  79. def precmd(self, line):
  80. """Print the line just entered for debugging purposes.
  81. Args:
  82. line which is simply returned unchanged.
  83. """
  84. if line.lower().find('password') >= 0:
  85. logging.debug('command: (command involving password not shown)')
  86. else:
  87. logging.debug('command: %s' % line)
  88. return line
  89. """
  90. ****************** Configuration variable-setting
  91. Commands:
  92. set
  93. """
  94. def ShowConfigVars(self):
  95. cvars = self._config.attrs.keys()
  96. cvars.sort()
  97. for var in cvars:
  98. print '%s:\t%s\n' % (var, self._config.attrs[var])
  99. def do_set(self, rest):
  100. args = rest.strip()
  101. if not len(args):
  102. self.help_set()
  103. return
  104. toks = rest.split(' ')
  105. # see if it's a real variable
  106. owner = self._config.FindOwner(toks[0])
  107. if owner == None:
  108. logging.error(messages.msg(messages.ERR_NO_SUCH_ATTR, toks[0]))
  109. return
  110. value = ' '.join(toks[1:])
  111. msg = owner.SetConfigVar(toks[0], value)
  112. if msg:
  113. logging.error(msg)
  114. def help_set(self):
  115. print messages.MSG_SET_ANY
  116. self.ShowConfigVars()
  117. """
  118. ****************** LDAP connections
  119. Commands:
  120. connect
  121. disconnect
  122. """
  123. # ******************* connect command
  124. def do_connect(self, unused_rest):
  125. try:
  126. if self.ldap_context.Connect():
  127. logging.error(messages.msg(messages.ERR_CONNECT_FAILED))
  128. else:
  129. logging.info(messages.msg(messages.MSG_CONNECTED,
  130. self.ldap_context.ldap_url))
  131. except utils.ConfigError, e:
  132. logging.error(str(e))
  133. def help_connect(self):
  134. print messages.msg(messages.HELP_CONNECT)
  135. # ******************* disconnect command
  136. def do_disconnect(self, unused_rest):
  137. if self.ldap_context.Disconnect():
  138. logging.error(messages.msg(messages.ERR_DISCONNECT_FAILED))
  139. logging.info(messages.msg(messages.MSG_DISCONNECTED,
  140. self.ldap_context.ldap_url))
  141. def help_disconnect(self):
  142. print messages.msg(messages.HELP_DISCONNECT)
  143. """
  144. ****************** LDAP searching and checking
  145. Commands:
  146. testFilter: test an LDAP filter, and get a suggested list of LDAP
  147. attributes to use, plus Google mappings
  148. updateUsers: pull in users from LDAP and update the UserDB,
  149. marking users with appropriate Google actions (added/exited/renamed/
  150. updated)
  151. """
  152. # ******************* testFilter command
  153. def do_testFilter(self, rest):
  154. (args, force_accept) = self._ProcessArgs(rest)
  155. if len(args):
  156. self.ldap_context.SetUserFilter(args)
  157. try:
  158. self.new_users = self.ldap_context.Search(attrlist=['cn'])
  159. except RuntimeError,e:
  160. logging.exception('**Error: %s\n', str(e))
  161. return
  162. except utils.ConfigError,e:
  163. logging.exception(str(e))
  164. return
  165. if not self.new_users or self.new_users.UserCount() == 0:
  166. print messages.msg(messages.MSG_FIND_USERS_RETURNED, "0")
  167. return
  168. print messages.msg(messages.MSG_FIND_USERS_RETURNED,
  169. str(self.new_users.UserCount()))
  170. # do it again with a small set, but get all attrs this time
  171. try:
  172. print messages.msg(messages.ERR_TEST_FILTER_SAMPLING)
  173. sample_full_attrs = self.ldap_context.Search(rest,
  174. min(10, self.new_users.UserCount()))
  175. if not sample_full_attrs:
  176. return
  177. # and since 'all attrs' doesn't include important stuff like
  178. # the modifyTimestamp, we have to do that separately:
  179. sample_top_attrs = self.ldap_context.Search(rest, 1, ['+'])
  180. if not sample_top_attrs:
  181. return
  182. except utils.ConfigError,e:
  183. logging.error(str(e))
  184. return
  185. # and then union the two sets:
  186. full_attr_set = sample_full_attrs.GetAttributes()
  187. top = sample_top_attrs.GetAttributes()
  188. full_attr_set.extend(top)
  189. print messages.msg(messages.MSG_TEST_FILTER_ATTRS)
  190. pp.pprint(full_attr_set)
  191. (self.trialAttrs, self.trialMappings) = sample_full_attrs.SuggestAttrs()
  192. print messages.msg(messages.MSG_SUGGESTED_ATTRS)
  193. lst = list(self.trialAttrs)
  194. lst.sort()
  195. for attr in lst:
  196. print '\t%s' % attr
  197. print messages.msg(messages.MSG_SUGGESTED_MAPPINGS)
  198. for (gattr, lattr) in self.trialMappings.iteritems():
  199. print "%-30s %s" % (gattr, lattr)
  200. self.users.SetTimestamp(userdb.SuggestTimestamp(full_attr_set))
  201. print messages.msg(messages.MSG_SUGGESTED_TIMESTAMP)
  202. if self.users.GetTimestampAttributeName():
  203. print '\t%s' % self.users.GetTimestampAttributeName()
  204. else:
  205. print messages.MSG_NO_TIMESTAMP
  206. self.users.primary_key = userdb.SuggestPrimaryKey(full_attr_set)
  207. if self.users.primary_key:
  208. print messages.msg(messages.MSG_SUGGESTED_PRIMARY_KEY)
  209. print '\t%s' % self.users.primary_key
  210. first = " "
  211. while first != "y" and first != "n":
  212. if not force_accept:
  213. ans = raw_input(messages.msg(messages.MSG_ACCEPT_SUGGESTIONS))
  214. else:
  215. ans = 'y'
  216. first = ans[:1].lower()
  217. if first == "y":
  218. self._SetSuggestedAttrs()
  219. self._SetSuggestedMappings()
  220. elif first == "n":
  221. print messages.msg(messages.MSG_SAID_NO_SUGGESTIONS)
  222. else:
  223. print messages.msg(messages.ERR_YES_OR_NO)
  224. def help_testFilter(self):
  225. print messages.msg(messages.HELP_TEST_FILTER)
  226. # ******************* updateUsers command
  227. def do_updateUsers(self, unused_rest):
  228. try:
  229. if not self.ldap_context.GetUserFilter():
  230. logging.error(messages.ERR_NO_USER_FILTER)
  231. return
  232. print messages.msg(messages.MSG_ADDING, self.ldap_context.GetUserFilter())
  233. self._ShowGoogleAttributes()
  234. last_update_time.beginNewRun()
  235. self.errors = False
  236. # add in the condition for "> lastUpdate"
  237. search_filter = self.ldap_context.ldap_user_filter
  238. self.last_update = None
  239. if last_update_time.get():
  240. self.last_update = self._TimeFromLDAPTime(last_update_time.get())
  241. logging.debug('last_update time=%s' % str(self.last_update))
  242. attrs = self.users.GetAttributes()
  243. directory_type = _GetDirectoryType(attrs)
  244. if self.last_update:
  245. search_filter = self._AndUpdateTime(search_filter,
  246. self.users.GetTimestampAttributeName(), self.last_update,
  247. directory_type)
  248. try:
  249. found_users = self.ldap_context.Search(filter_arg=search_filter,
  250. attrlist=attrs)
  251. except RuntimeError,e:
  252. logging.exception(str(e))
  253. return
  254. if not found_users or found_users.UserCount() == 0:
  255. print messages.msg(messages.MSG_FIND_USERS_RETURNED, "0")
  256. if found_users:
  257. # we need to compute the Google attrs now, since
  258. # userdb.AnalyzeChangedUsers uses that:
  259. self.users.MapGoogleAttrs(found_users)
  260. (adds, mods, renames) = self.users.AnalyzeChangedUsers(found_users)
  261. # mark new uses as "to be added to Google"
  262. for dn in adds:
  263. found_users.SetGoogleAction(dn, 'added')
  264. for dn in mods:
  265. found_users.SetGoogleAction(dn, 'updated')
  266. for dn in renames:
  267. found_users.SetGoogleAction(dn, 'renamed')
  268. self.users.MergeUsers(found_users)
  269. if adds:
  270. print messages.msg(messages.MSG_NEW_USERS_ADDED, (str(len(adds)),
  271. str(self.users.UserCount())))
  272. if mods:
  273. print messages.msg(messages.MSG_UPDATED_USERS_MARKED,
  274. (str(len(mods))))
  275. if renames:
  276. print messages.msg(messages.MSG_RENAMED_USERS_MARKED,
  277. (str(len(renames))))
  278. # find exited users & lock their accounts
  279. self._FindExitedUsers()
  280. except utils.ConfigError, e:
  281. logging.error(str(e))
  282. def help_updateUsers(self):
  283. print messages.msg(messages.HELP_ADD_USERS)
  284. """
  285. ****************** Viewing the users
  286. Commands:
  287. showLastUpdate: displays the time of the last updateUsers
  288. showUsers: (very limited) display of contents of the UserDB
  289. summarizeUsers: shows total users, and #'s of added/exited/renamed/updated
  290. """
  291. # ******************* showLastUpdate command
  292. def do_showLastUpdate(self, unused_rest):
  293. self.last_update = last_update_time.get()
  294. if not self.last_update:
  295. print messages.MSG_SHOW_NO_LAST_UPDATE
  296. else:
  297. try:
  298. self.last_update = float(self.last_update)
  299. except ValueError:
  300. logging.exception('bad update time: %s', str(self.last_update))
  301. self.last_update = 0
  302. tstr = time.asctime(time.localtime())
  303. print messages.msg(messages.MSG_SHOW_LAST_UPDATE, tstr)
  304. def help_showLastUpdate(self):
  305. print messages.msg(messages.HELP_SHOW_LAST_UPDATE)
  306. # ******************* showUsers command
  307. def do_showUsers(self, rest):
  308. args = rest.strip()
  309. start = 1
  310. user_count = self.users.UserCount()
  311. if not user_count:
  312. logging.info(messages.ERR_NO_USERS)
  313. return
  314. end = self.users.UserCount()
  315. if args:
  316. nums = self._GetNumArgs(rest, 2)
  317. if not nums:
  318. logging.error(messages.msg(messages.ERR_SHOW_USERS_ARGS))
  319. return
  320. start = nums[0]
  321. if len(nums) > 1:
  322. end = nums[1]
  323. else:
  324. end = start
  325. user_count = end - start + 1
  326. # don't just spew out 20,000 users without a warning:
  327. if user_count > 10:
  328. ans = raw_input(messages.msg(messages.ERR_TOO_MANY_USERS,
  329. str(user_count)))
  330. first = ans[:1].lower()
  331. if first != "y":
  332. return
  333. dns = self.users.UserDNs()
  334. print "Display new users %d to %d" % (start, end)
  335. for ix in xrange(start-1, end):
  336. print "%d: %s" % (ix + 1, dns[ix])
  337. pp.pprint(self.users.LookupDN(dns[ix]))
  338. def help_showUsers(self):
  339. print messages.msg(messages.HELP_SHOW_USERS)
  340. # ******************* summarizeUsers command
  341. def do_summarizeUsers(self, unused_rest):
  342. total = 0
  343. added = 0
  344. exited = 0
  345. updated = 0
  346. renamed = 0
  347. for (dn, attrs) in self.users.db.iteritems():
  348. total += 1
  349. if 'meta-Google-action' in attrs:
  350. action = attrs['meta-Google-action']
  351. if action == 'added':
  352. added += 1
  353. elif action == 'exited':
  354. exited += 1
  355. elif action == 'renamed':
  356. renamed += 1
  357. elif action == 'updated':
  358. updated += 1
  359. print messages.msg(messages.MSG_USER_SUMMARY,
  360. (str(total), str(added), str(exited), str(renamed),
  361. str(updated)))
  362. def help_summarizeUsers(self):
  363. print messages.msg(messages.HELP_SUMMARIZE_USERS)
  364. """
  365. ****************** Managing attributes and mappings
  366. Commands:
  367. attrList: display & edit the list of attributes
  368. mapGoogleAttribute: set the mapping from LDAP attributes to
  369. Google attributes
  370. markUsers: manually set the 'Google action' for one or more users
  371. """
  372. # ******************* attrList command
  373. def do_attrList(self, rest):
  374. args = rest.strip()
  375. if args:
  376. toks = args.split(" ")
  377. if toks[0].lower() == "show":
  378. attrs = self.users.GetAttributes()
  379. print messages.msg(messages.MSG_MAPPINGS)
  380. pp.pprint(attrs)
  381. self._ShowGoogleAttributes()
  382. return
  383. # if here, we require a second argument
  384. if len(toks) < 2:
  385. logging.error(messages.msg(messages.MSG_USAGE_ATTR_LIST))
  386. return
  387. attr = toks[1]
  388. if toks[0].lower() == "add":
  389. self.users.AddAttribute(attr)
  390. elif toks[0].lower() == "remove":
  391. if not attr in self.users.GetAttributes():
  392. logging.error(messages.msg(messages.ERR_NO_SUCH_ATTR, attr))
  393. return
  394. count = self.users.RemoveAttribute(attr)
  395. print messages.msg(messages.MSG_ATTR_REMOVED, (attr, str(count)))
  396. else:
  397. logging.error(messages.msg(messages.MSG_USAGE_ATTR_LIST))
  398. def help_attrList(self):
  399. print messages.msg(messages.HELP_ATTR_LIST)
  400. # ******************* mapGoogleAttribute command
  401. def do_mapGoogleAttribute(self, rest):
  402. line = rest.strip()
  403. if not line:
  404. print messages.msg(messages.ERR_MAP_ATTR)
  405. return
  406. toks = line.split(" ")
  407. attr = toks[0]
  408. if not attr in self.users.GetGoogleMappings():
  409. print messages.msg(messages.ERR_NO_SUCH_ATTR, attr)
  410. return
  411. if len(toks) > 1:
  412. mapping = rest.replace(attr, "", 1)
  413. else:
  414. mapping = raw_input(messages.MSG_ENTER_EXPRESSION)
  415. # test the expression
  416. print messages.msg(messages.MSG_TESTING_MAPPING)
  417. err = self.users.TestMapping(mapping.strip())
  418. if err:
  419. logging.error(messages.msg(messages.ERR_MAPPING_FAILED))
  420. logging.error(err)
  421. return
  422. self.users.MapAttr(attr, mapping)
  423. print messages.MSG_DONE
  424. def help_mapGoogleAttribute(self):
  425. print messages.HELP_MAP_ATTR
  426. # ******************* markUsers command
  427. def do_markUsers(self, rest):
  428. args = rest.strip()
  429. if not args:
  430. logging.error(messages.msg(messages.ERR_MARK_USERS))
  431. logging.error(messages.msg(messages.HELP_MARK_USERS))
  432. return
  433. toks = args.split(' ')
  434. if len(toks) < 2 or len(toks) > 3:
  435. logging.error(messages.msg(messages.ERR_MARK_USERS))
  436. logging.error(messages.msg(messages.HELP_MARK_USERS))
  437. return
  438. first_str = toks[0]
  439. second_str = None
  440. second = None
  441. if len(toks) == 3:
  442. second_str = toks[1]
  443. action = toks[2]
  444. else:
  445. action = toks[1]
  446. try:
  447. s = first_str
  448. first = int(first_str)
  449. second = first
  450. if second_str:
  451. s = second_str
  452. second = int(second_str)
  453. except ValueError:
  454. logging.exception('%s\n' % messages.msg(messages.ERR_ENTER_NUMBER, s))
  455. return
  456. # parse the action
  457. if action != 'added' and action != 'exited' and action != 'update':
  458. logging.error(messages.msg(messages.ERR_MARK_USERS_ACTION))
  459. logging.error(messages.msg(messages.HELP_MARK_USERS))
  460. return
  461. else:
  462. dns = self.users.UserDNs()
  463. if first < 0 or first > len(dns):
  464. logging.error(messages.msg(messages.ERR_NUMBER_OUT_OF_RANGE, first_str))
  465. return
  466. if second:
  467. if second < 0 or second > len(dns) or second < first:
  468. logging.error(messages.msg(messages.ERR_NUMBER_OUT_OF_RANGE,
  469. second_str))
  470. return
  471. for ix in xrange(first, second + 1):
  472. self.users.SetGoogleAction(dns[ix], action)
  473. def help_markUsers(self):
  474. print messages.HELP_MARK_USERS
  475. """
  476. ****************** Synchronizing with Google
  477. Commands:
  478. syncOneUser: sync a single user with UserDB AND with Google. This is a
  479. two-way sync, meaning it fetches the user's information from Google and
  480. compares it to the current LDAP information.
  481. syncAllUsers: sync all users with Google. This is a one-way sync, i.e. it
  482. does not fetch the list of users from Google and compare. (in a future
  483. version of the Provisioning API, this will be feasible; right now (late
  484. 2006), it would be too slow.
  485. """
  486. def do_syncOneUser(self, rest):
  487. """ This is the one example of a two-way sync in this tool. It compares
  488. LDAP to the UserDB, and it also goes to GAFYD for the status of the
  489. user.
  490. Args:
  491. -f: "force" acceptance by the user. This is mainly for the unittest
  492. so it doesn't wait for human input. End users: use at your own risk!
  493. """
  494. (args, force_accept) = self._ProcessArgs(rest)
  495. if not args:
  496. logging.error(messages.ERR_SUPPLY_VALID_USER)
  497. return
  498. the_user = self._FindOneUser(args)
  499. old_username = None
  500. if not the_user: # not found in LDAP
  501. # see if it's in UserDB, but deleted from LDAP:
  502. dn = self._AnalyzeUserMissingInLDAP(args)
  503. if not dn:
  504. return
  505. the_user = self.users.RestrictUsers(dn)
  506. act = 'exited'
  507. else: # it IS in LDAP; see what's up with it
  508. dn = the_user.UserDNs()[0]
  509. # save the GoogleUsername value before we (potentially) overwrite it:
  510. attrs = self.users.LookupDN(dn)
  511. if attrs:
  512. if 'GoogleUsername' in attrs:
  513. old_username = attrs['GoogleUsername']
  514. self.users.MapGoogleAttrs(the_user)
  515. (added, modded, renamed) = self.users.AnalyzeChangedUsers(the_user)
  516. act = None
  517. if added:
  518. act = 'added'
  519. elif modded:
  520. act = 'updated'
  521. elif renamed:
  522. act = 'renamed'
  523. the_user.SetGoogleAction(dn, act)
  524. self.users.MergeUsers(the_user)
  525. attrs = self.users.LookupDN(dn)
  526. print messages.msg(messages.MSG_USER_IS_NOW, dn)
  527. if 'GoogleUsername' not in attrs:
  528. logging.error(messages.msg(messages.ERR_NO_ATTR_FOR_USER,
  529. 'GoogleUsername'))
  530. return
  531. username = attrs['GoogleUsername']
  532. # what if this is a rename?
  533. old_user_rec = None
  534. if old_username:
  535. if old_username != username:
  536. old_user_rec = self._FetchOneUser(old_username)
  537. user_rec = self._FetchOneUser(username)
  538. else: # new user
  539. user_rec = self._FetchOneUser(username)
  540. act = self._TwoWayCompare(dn, user_rec, old_user_rec)
  541. if not act:
  542. print messages.MSG_UP_TO_DATE
  543. return
  544. # give admin a chance to approve the action:
  545. print messages.msg(messages.MSG_RECOMMENDED_ACTION_IS, act)
  546. if not force_accept: # normal case: ask the human
  547. ans = raw_input(messages.MSG_PROCEED_TO_APPLY)
  548. if ans[:1] != messages.CHAR_YES:
  549. return
  550. self.sync_google.DoAction(act, dn)
  551. def help_syncOneUser(self):
  552. print messages.HELP_SYNC_ONE_USER
  553. # ******************* syncAllUsers command
  554. def do_syncAllUsers(self, rest):
  555. args = rest.strip().lower()
  556. if args:
  557. if args == 'added':
  558. actions = ['added']
  559. elif args == 'exited':
  560. actions = ['exited']
  561. elif args == 'updated':
  562. actions = ['updated']
  563. elif args == 'renamed':
  564. actions = ['renamed']
  565. elif args == 'all':
  566. actions = self.sync_google.google_operations
  567. else:
  568. logging.error(messages.msg(messages.ERR_SYNC_USERS_ACTION))
  569. return
  570. else:
  571. actions = self.sync_google.google_operations
  572. # be sure we can connect, before we spawn a bunch of threads that'll try:
  573. errs = self.sync_google.TestConnectivity()
  574. if errs:
  575. logging.error(messages.msg(messages.ERR_CONNECTING_GOOGLE, errs))
  576. return
  577. try:
  578. for action in actions:
  579. stats = self.sync_google.DoAction(action)
  580. if stats is not None:
  581. self._ShowSyncStats(stats)
  582. except utils.ConfigError, e:
  583. logging.error(str(e))
  584. return
  585. last_update_time.updateIfNoErrors()
  586. def help_syncAllUsers(self):
  587. print messages.HELP_SYNC_USERS_GOOGLE
  588. """
  589. ****************** Reading and writing the users to a file
  590. Commands:
  591. readUsers
  592. writeUsers
  593. """
  594. # ******************* readUsers command
  595. def do_readUsers(self, rest):
  596. args = rest.strip()
  597. if not args:
  598. logging.error(messages.MSG_GIVE_A_FILE_NAME)
  599. return
  600. else:
  601. fname = rest.split(" ")[0]
  602. print messages.msg(messages.MSG_READ_USERS, fname)
  603. try:
  604. self.users.ReadDataFile(fname)
  605. except RuntimeError, e:
  606. logging.exception(str(e))
  607. return
  608. print messages.msg(messages.MSG_DONE)
  609. def help_readUsers(self):
  610. print messages.msg(messages.HELP_READ_USERS)
  611. # ******************* writeUsers command
  612. def do_writeUsers(self, rest):
  613. args = rest.strip()
  614. if not args:
  615. logging.error(messages.MSG_GIVE_A_FILE_NAME)
  616. return
  617. else:
  618. fname = rest.split(" ")[0]
  619. print messages.msg(messages.MSG_WRITE_USERS, fname)
  620. try:
  621. rejected_attrs = self.users.WriteDataFile(fname)
  622. except RuntimeError, e:
  623. logging.exception(str(e))
  624. return
  625. print messages.msg(messages.MSG_DONE)
  626. if rejected_attrs and len(rejected_attrs):
  627. print messages.MSG_REJECTED_ATTRS
  628. for attr in rejected_attrs:
  629. print '\t%s' % attr
  630. def help_writeUsers(self):
  631. print messages.msg(messages.HELP_WRITE_USERS)
  632. """
  633. ****************** Miscellaneous commands
  634. Commands:
  635. batch: a file of commands can be executed as a batch
  636. stop: exit the command interpreter
  637. """
  638. # ******************* batch command
  639. def do_batch(self, rest):
  640. args = rest.strip()
  641. if not args:
  642. logging.error(messages.msg(messages.ERR_BATCH_ARG_NEEDED))
  643. return
  644. fname = args.split(" ")[0]
  645. if not os.path.exists(fname):
  646. logging.error(messages.msg(messages.ERR_FILE_NOT_FOUND, fname))
  647. return
  648. f = open(fname, "r")
  649. for line in f.readlines():
  650. print line
  651. self.onecmd(line)
  652. f.close()
  653. def help_batch(self):
  654. print messages.msg(messages.HELP_BATCH)
  655. # ******************* stop command
  656. def do_stop(self, unused_rest):
  657. print messages.msg(messages.MSG_STOPPING)
  658. # don't know where this is documented, but returning something
  659. # other than None stops the cmdloop()
  660. return -1
  661. def help_stop(self):
  662. print messages.msg(messages.HELP_STOP)
  663. def do_EOF(self, rest):
  664. return self.do_stop(rest)
  665. """
  666. ****************** Utility methods
  667. _AnalyzeUserMissingInLDAP
  668. _AndUpdateTime
  669. _ChooseFromList
  670. _CompareWithGoogle
  671. _FetchOneUser
  672. _FindExitedUsers
  673. _FindOneUser
  674. _GetNumArgs
  675. _ProcessArgs
  676. _SetSuggestedAttrs
  677. _SetSuggestedMappings
  678. _ShowGoogleAttributes
  679. _SplitExpression
  680. _TwoWayCompare
  681. _ValidateLDAPTime
  682. """
  683. def _AnalyzeUserMissingInLDAP(self, args):
  684. """ For the syncOneUser command: if the admin has entered an
  685. expression which didn't map to any LDAP users, figure out what's
  686. what. Is it a user who's in the UserDB but no longer in LDAP?
  687. If so, that's probably an exited.
  688. Args:
  689. args: stripped version of the expression the admin entered
  690. Returns:
  691. dn of user, or None if a user in UserDB couldn't be found
  692. Side effects:
  693. displays message to the admin if more than one DN matches
  694. the expression, and waits for him to choose one.
  695. """
  696. # see if it's in UserDB, but deleted from LDAP:
  697. comps = self._SplitExpression(args)
  698. if not comps:
  699. return
  700. (attr, val) = comps
  701. if not attr:
  702. return
  703. dns = self.users.LookupAttrVal(attr, val)
  704. if not dns:
  705. return None
  706. print messages.msg(messages.MSG_FOUND_IN_USERDB, str(len(dns)))
  707. return self._ChooseFromList(dns)
  708. def _AndUpdateTime(self, search_filter, timeStampAttr, ts, directoryType):
  709. """ AND in the "modifyTimestamp > time" condition
  710. to the filter
  711. Args:
  712. search_filter: LDAP filter expression
  713. timeStampAttr: name of LDAP attribute containing the timestamp
  714. ts: what the value of the attribute indicated by timeStampAttr must be
  715. greater than.
  716. directoryType: one of 'ad', 'openldap', 'eDirectory'. This is used to
  717. deal with differences in directories around querying
  718. modifyTimestamp
  719. """
  720. if self._testing_last_update:
  721. stamp = self._testing_last_update
  722. else:
  723. stamp = ts
  724. # NOTE: The following table summarizes the format the modifyTimestamp
  725. # filter needs to be in for various directories
  726. #
  727. # %sZ %s.Z %s.0Z
  728. # ad N Y Y
  729. # edirectory Y N N
  730. # openldap Y N Y
  731. if directoryType == 'ad':
  732. cond = '%s>=%s.Z' % (timeStampAttr, time.strftime('%Y%m%d%H%M%S',
  733. time.localtime(stamp)))
  734. else:
  735. cond = '%s>=%sZ' % (timeStampAttr, time.strftime('%Y%m%d%H%M%S',
  736. time.localtime(stamp)))
  737. s = "(&%s(%s))" % (search_filter, cond)
  738. logging.debug("new filter is: %s" % s)
  739. return s
  740. def _ChooseFromList(self, dns):
  741. """ Utility to present the user with a list of DNs and ask them
  742. to choose one
  743. Args:
  744. dns: list of DNs
  745. Return:
  746. dn of the one chosen, or None if none chosen
  747. """
  748. count = len(dns)
  749. if count == 1:
  750. return dns[0]
  751. else:
  752. if count > MAX_USER_DISPLAY:
  753. logging.info(messages.msg(messages.MSG_HERE_ARE_FIRST_N,
  754. str(MAX_USER_DISPLAY)))
  755. limit = MAX_USER_DISPLAY
  756. for i in xrange(limit):
  757. print '%d: %s' % (i, dns[i])
  758. ans = raw_input(messages.MSG_WHICH_USER)
  759. try:
  760. num = int(ans)
  761. if num < 0 or num >= limit:
  762. return None
  763. return dns[num]
  764. except ValueError,e:
  765. logging.error(str(e))
  766. return None
  767. def _CompareWithGoogle(self, attrs, google_result):
  768. """ Compare the list of attributes for a user in the users database with
  769. the results from Google. (Obviously this only compares the Google
  770. attributes)
  771. """
  772. for (attr, gattr) in self.users.google_val_map.iteritems():
  773. if attr not in attrs or not attrs[attr]:
  774. if gattr in google_result and google_result[gattr]:
  775. return 1
  776. else:
  777. continue
  778. if gattr not in google_result or not google_result[gattr]:
  779. if attr in attrs and attrs[attr]:
  780. return -1
  781. else:
  782. continue
  783. if attrs[attr].lower() < google_result[gattr].lower():
  784. return -1
  785. elif attrs[attr].lower() > google_result[gattr].lower():
  786. return 1
  787. return 0
  788. def _FetchOneUser(self, username):
  789. """ Fetch info on a single username from Google, with user feedback
  790. """
  791. print messages.msg(messages.MSG_LOOKING_UP, username)
  792. user_rec = self.sync_google.FetchOneUser(username)
  793. if not user_rec:
  794. print messages.msg(messages.ERR_USER_NOT_FOUND, username)
  795. else:
  796. print messages.MSG_GOOGLE_RETURNED
  797. #pp.pprint(user_rec)
  798. self._PrintGoogleUserRec(user_rec)
  799. return user_rec
  800. def _FindExitedUsers(self):
  801. """
  802. Finding "exited" users: if we have a special filter for that, use it.
  803. Else do the search without the "> lastUpdate" filter, to find
  804. users no longer in the DB.
  805. Even if we DO have a ldap_disabled_filter, still check for deleted
  806. entries, since you never know what might have happened.
  807. """
  808. total_exits = 0
  809. if (self.ldap_context.ldap_disabled_filter and
  810. self.users.GetTimestampAttributeName()):
  811. attrs = self.users.GetAttributes()
  812. directory_type = _GetDirectoryType(attrs)
  813. search_filter = self._AndUpdateTime(
  814. self.ldap_context.ldap_disabled_filter,
  815. self.users.GetTimestampAttributeName(), self.last_update,
  816. directory_type)
  817. try:
  818. logging.debug(messages.msg(messages.MSG_FIND_EXITS,
  819. self.ldap_context.ldap_disabled_filter))
  820. userdb_exits = self.ldap_context.Search(filter_arg=search_filter,
  821. attrlist=attrs)
  822. if not userdb_exits:
  823. return
  824. logging.debug('userdb_exits=%s' % userdb_exits.UserDNs())
  825. exited_users = userdb_exits.UserDNs()
  826. for dn in exited_users:
  827. # Note: users previously marked added can be reset to exited
  828. # if they match the exit filter. This ensures
  829. # added_user_google_action is never called on a locked user that
  830. # exists in Google Apps
  831. self.users.SetGoogleAction(dn, 'exited')
  832. total_exits += 1
  833. except RuntimeError,e:
  834. logging.exception(str(e))
  835. return
  836. # Also: find ALL the users, and see which old ones are no longer
  837. # there:
  838. exited_users = self.users.FindDeletedUsers(self.ldap_context)
  839. if not exited_users:
  840. return
  841. logging.debug('deleted users=%s' % str(exited_users))
  842. for dn in exited_users:
  843. self.users.SetIfUnsetGoogleAction(dn, 'exited')
  844. total_exits += 1
  845. if total_exits:
  846. logging.info(messages.msg(messages.MSG_OLD_USERS_MARKED,
  847. str(total_exits)))
  848. def _FindOneUser(self, expr):
  849. """ Utility for determining a single DN from a (presumably user-typed)
  850. search expression. If more than one hit, asks the user to choose one.
  851. Args:
  852. expr: a user-typed search expression
  853. Return:
  854. a new instance of UserDB containing just the single DN, or none if
  855. none could be found and selected by the user
  856. """
  857. try:
  858. user_hits = self.ldap_context.Search(filter_arg=expr,
  859. attrlist=self.users.GetAttributes())
  860. except RuntimeError, e:
  861. logging.error(str(e))
  862. return
  863. if not user_hits:
  864. print messages.msg(messages.MSG_FIND_USERS_RETURNED, '0')
  865. count = user_hits.UserCount()
  866. dns = user_hits.UserDNs()
  867. if count == 0:
  868. print messages.msg(messages.MSG_FIND_USERS_RETURNED, str(count))
  869. return None
  870. elif count > 1:
  871. print messages.msg(messages.MSG_FIND_USERS_RETURNED, str(count))
  872. dn = self._ChooseFromList(dns)
  873. else: # the preferred case: just one hit to the query
  874. dn = dns[0]
  875. if not dn:
  876. logging.error(messages.msg(messages.MSG_FIND_USERS_RETURNED, '0'))
  877. return None
  878. return self.users.RestrictUsers(dn, user_hits)
  879. def _GetNumArgs(self, rest, countMax):
  880. """ utility routine to extract up to countMax integers from
  881. the arguments. Returns None if too many, or they're not ints
  882. Args:
  883. rest : as passed by Cmd module
  884. countMax : the most ints allowed
  885. Return:
  886. list of the args
  887. """
  888. result = []
  889. toks = rest.strip().split(" ")
  890. if len(toks) > countMax:
  891. return None
  892. for tok in toks:
  893. try:
  894. result.append(int(tok))
  895. except ValueError:
  896. return None
  897. return result
  898. def _PrintGoogleUserRec(self, rec):
  899. if not rec:
  900. return
  901. for (key, val) in rec.iteritems():
  902. print '%-30s: %s' % (str(key), str(val))
  903. def _ProcessArgs(self, rest):
  904. """ Utility for commands that may have a '-f' flag, for
  905. 'force a yes to any question to the user' (which is mainly for
  906. the unit-test).
  907. Args:
  908. rest: as passed by the cmd.Cmd module
  909. Returns:
  910. lower-cased, stripped version of 'rest', with the -f removed
  911. if it was there
  912. boolean for whether -f was there
  913. """
  914. force = False
  915. args = rest.strip()
  916. if args.find('-f') >= 0:
  917. force = True
  918. args = args.replace('-f', '').strip()
  919. args = args.strip().lower()
  920. return (args, force)
  921. def _SetSuggestedAttrs(self):
  922. self.users.RemoveAllAttributes()
  923. for attr in self.trialAttrs:
  924. self.users.AddAttribute(attr)
  925. if self.users.GetTimestampAttributeName():
  926. self.users.AddAttribute(self.users.GetTimestampAttributeName())
  927. del self.trialAttrs
  928. def _SetSuggestedMappings(self):
  929. for (gattr, expr) in self.trialMappings.iteritems():
  930. self.users.MapAttr(gattr, expr)
  931. del self.trialMappings
  932. def _ShowGoogleAttributes(self):
  933. for (gattr, lattr) in self.users.GetGoogleMappings().iteritems():
  934. print "%-30s %s" % (gattr, lattr)
  935. def _ShowSyncStats(self, stats):
  936. """ Display the results of a "sync to Google" operation. The
  937. assumption is that 'stats' will contain all members of
  938. ThreadStats.stat_names but that some will be zero. If either
  939. <op>s or <op>_fails is non-zero, then a line concerning <op>
  940. will be displayed, op in {'add', 'exit', 'rename', 'update'}
  941. Args:
  942. stats: return value of all sync_google.Do_<operation>. An
  943. instance of sync_google.ThreadStats.
  944. """
  945. if stats['adds'] > 0 or stats['add_fails'] > 0:
  946. print messages.msg(messages.MSG_ADD_RESULTS, (stats['adds'],
  947. stats['add_fails']))
  948. if stats['exits'] > 0 or stats['exit_fails'] > 0:
  949. print messages.msg(messages.MSG_EXITED_RESULTS, (stats['exits'],
  950. stats['exit_fails']))
  951. if stats['renames'] > 0 or stats['rename_fails'] > 0:
  952. print messages.msg(messages.MSG_RENAME_RESULTS, (stats['renames'],
  953. stats['rename_fails']))
  954. if stats['updates'] > 0 or stats['update_fails'] > 0:
  955. print messages.msg(messages.MSG_UPDATE_RESULTS, (stats['updates'],
  956. stats['update_fails']))
  957. def _SplitExpression(self, expr):
  958. """ For an admin-typed expression, e.g. givenName=joe, split it
  959. into two components around the equals sign.
  960. Args:
  961. expr: as entered by the admin
  962. Returns: (None, None) if the expression couldn't be split, or
  963. attr: name of the attribute
  964. value: value
  965. """
  966. ix = expr.find('=')
  967. if ix <= 0:
  968. logging.error(messages.msg(messages.ERR_CANT_USE_EXPR, expr))
  969. return (None, None)
  970. attr = expr[:ix].strip()
  971. val = expr[ix + 1:].strip()
  972. return (attr, val)
  973. def _TwoWayCompare(self, dn, google_result, google_result_old=None):
  974. """ Having retrieved the GAFYD result for a given user (which may be None),
  975. determine the correct thing to do, in consulation with the user.
  976. NOTE that the UserDB already has our analysis of what needs to be done for
  977. this user, but that was based solely on our own data. Now we have the
  978. google_result as well, so we get to figure it out more accurately.
  979. The case are, if we consider the user:
  980. 'added'
  981. - google already has it, all the same data.
  982. Nothing to do
  983. - google does not have it.
  984. 'added' is correct
  985. - google has it, but some data is different.
  986. change to 'updated'
  987. 'exited'
  988. - google does not have it at all.
  989. Nothing to do
  990. - google has it.
  991. 'exited' is correct
  992. 'updated':
  993. - Google does not have it.
  994. change to 'added'
  995. - Google has it and all data is the same as we show it.
  996. Nothing to do
  997. - Google has it and it needs updating
  998. 'updated' is correct
  999. 'renamed'
  1000. - Google already has the new username.
  1001. Nothing to do (it might be we should update the new one, but not in
  1002. this version)
  1003. - Google does not have the old username:
  1004. Nothing to do
  1005. - Google has the old username
  1006. 'renamed' is correct
  1007. Args:
  1008. dn: DN of the user
  1009. google_result: return value from provisioning.RetrieveAccount()
  1010. google_result_old: return value from provisioning.RetrieveAccount()
  1011. Return:
  1012. act: one of ('added','exited','renamed','updated', None)
  1013. """
  1014. attrs = self.users.LookupDN(dn)
  1015. for gattr in ['GoogleFirstName', 'GoogleLastName', 'GoogleUsername',
  1016. 'GooglePassword', 'GoogleQuota']:
  1017. if gattr not in attrs:
  1018. logging.error(messages.msg(messages.ERR_NO_ATTR_FOR_USER, gattr))
  1019. return
  1020. if 'meta-Google-action' not in attrs:
  1021. act = None
  1022. else:
  1023. act = attrs['meta-Google-action']
  1024. # this code follows the comments at the top, rigorously. If you change
  1025. # either, please change the other
  1026. if act == 'added':
  1027. if google_result:
  1028. comp = self._CompareWithGoogle(attrs, google_result)
  1029. if not comp:
  1030. act = None
  1031. else:
  1032. act = 'added'
  1033. elif act == 'exited':
  1034. if not google_result:
  1035. act = None
  1036. elif act == 'updated':
  1037. if not google_result:
  1038. act = 'added'
  1039. else:
  1040. comp = self._CompareWithGoogle(attrs, google_result)
  1041. if not comp:
  1042. act = None
  1043. elif act == 'renamed':
  1044. if google_result:
  1045. act = None
  1046. elif not google_result_old:
  1047. act = None
  1048. return act
  1049. def _TimeFromLDAPTime(self, num):
  1050. """ Take a time from LDAP, like 20061207194034.0Z, and convert
  1051. to a regular Python time module time.
  1052. Args:
  1053. num: an LDAP time-valued attribute, or a float
  1054. Return:
  1055. floating point time value, per the time module
  1056. """
  1057. if not num:
  1058. return None
  1059. stime = str(num)
  1060. try:
  1061. tups = time.strptime(stime[:stime.find('.')], '%Y%m%d%H%M%S')
  1062. ft = time.mktime(tups)
  1063. except ValueError:
  1064. logging.error('Unable to convert %s to a time' % stime)
  1065. ft = None
  1066. return ft
  1067. def _GetDirectoryType(attrs):
  1068. directory_type = 'openldap'
  1069. if 'sAMAccountName' in attrs:
  1070. directory_type = 'ad'
  1071. return directory_type