HostCleanup.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. #!/usr/bin/env python
  2. '''
  3. Licensed to the Apache Software Foundation (ASF) under one
  4. or more contributor license agreements. See the NOTICE file
  5. distributed with this work for additional information
  6. regarding copyright ownership. The ASF licenses this file
  7. to you under the Apache License, Version 2.0 (the
  8. "License"); you may not use this file except in compliance
  9. with the License. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing, software
  12. distributed under the License is distributed on an "AS IS" BASIS,
  13. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. See the License for the specific language governing permissions and
  15. limitations under the License.
  16. '''
  17. import sys
  18. # For compatibility with different OSes
  19. # Edit PYTHONPATH to be able to import common_functions
  20. sys.path.append("/usr/lib/python2.6/site-packages/")
  21. import os
  22. import string
  23. import subprocess
  24. import logging
  25. import shutil
  26. import platform
  27. import fnmatch
  28. import ConfigParser
  29. import optparse
  30. import shlex
  31. import datetime
  32. from AmbariConfig import AmbariConfig
  33. from ambari_commons import OSCheck, OSConst
  34. if OSCheck.get_os_family() != OSConst.WINSRV_FAMILY:
  35. from pwd import getpwnam
  36. logger = logging.getLogger()
  37. PACKAGE_ERASE_CMD = {
  38. "redhat": "yum erase -y {0}",
  39. "suse": "zypper -n -q remove {0}",
  40. "ubuntu": "/usr/bin/apt-get -y -q remove {0}"
  41. }
  42. USER_ERASE_CMD = "userdel -rf {0}"
  43. GROUP_ERASE_CMD = "groupdel {0}"
  44. PROC_KILL_CMD = "kill -9 {0}"
  45. ALT_DISP_CMD = "alternatives --display {0}"
  46. ALT_ERASE_CMD = "alternatives --remove {0} {1}"
  47. REPO_PATH_RHEL = "/etc/yum.repos.d"
  48. REPO_PATH_SUSE = "/etc/zypp/repos.d/"
  49. SKIP_LIST = []
  50. HOST_CHECK_FILE_NAME = "hostcheck.result"
  51. OUTPUT_FILE_NAME = "hostcleanup.result"
  52. PACKAGE_SECTION = "packages"
  53. PACKAGE_KEY = "pkg_list"
  54. USER_SECTION = "users"
  55. USER_KEY = "usr_list"
  56. USER_HOMEDIR_KEY = "usr_homedir_list"
  57. USER_HOMEDIR_SECTION = "usr_homedir"
  58. REPO_SECTION = "repositories"
  59. REPOS_KEY = "repo_list"
  60. DIR_SECTION = "directories"
  61. ADDITIONAL_DIRS = "additional_directories"
  62. DIR_KEY = "dir_list"
  63. CACHE_FILES_PATTERN = {
  64. 'alerts': ['*.json']
  65. }
  66. PROCESS_SECTION = "processes"
  67. PROCESS_KEY = "proc_list"
  68. ALT_SECTION = "alternatives"
  69. ALT_KEYS = ["symlink_list", "target_list"]
  70. HADOOP_GROUP = "hadoop"
  71. FOLDER_LIST = ["/tmp"]
  72. # Additional path patterns to find existing directory
  73. DIRNAME_PATTERNS = [
  74. "/tmp/hadoop-", "/tmp/hsperfdata_"
  75. ]
  76. # resources that should not be cleaned
  77. REPOSITORY_BLACK_LIST = ["ambari.repo"]
  78. PACKAGES_BLACK_LIST = ["ambari-server", "ambari-agent"]
  79. class HostCleanup:
  80. def resolve_ambari_config(self):
  81. try:
  82. config = AmbariConfig()
  83. if os.path.exists(AmbariConfig.getConfigFile()):
  84. config.read(AmbariConfig.getConfigFile())
  85. else:
  86. raise Exception("No config found, use default")
  87. except Exception, err:
  88. logger.warn(err)
  89. return config
  90. def get_additional_dirs(self):
  91. resultList = []
  92. dirList = set()
  93. for patern in DIRNAME_PATTERNS:
  94. dirList.add(os.path.dirname(patern))
  95. for folder in dirList:
  96. for dirs in os.walk(folder):
  97. for dir in dirs:
  98. for patern in DIRNAME_PATTERNS:
  99. if patern in dir:
  100. resultList.append(dir)
  101. return resultList
  102. def do_cleanup(self, argMap=None):
  103. if argMap:
  104. packageList = argMap.get(PACKAGE_SECTION)
  105. userList = argMap.get(USER_SECTION)
  106. homeDirList = argMap.get(USER_HOMEDIR_SECTION)
  107. dirList = argMap.get(DIR_SECTION)
  108. repoList = argMap.get(REPO_SECTION)
  109. procList = argMap.get(PROCESS_SECTION)
  110. alt_map = argMap.get(ALT_SECTION)
  111. additionalDirList = self.get_additional_dirs()
  112. if userList and not USER_SECTION in SKIP_LIST:
  113. userIds = self.get_user_ids(userList)
  114. if procList and not PROCESS_SECTION in SKIP_LIST:
  115. logger.info("\n" + "Killing pid's: " + str(procList) + "\n")
  116. self.do_kill_processes(procList)
  117. if packageList and not PACKAGE_SECTION in SKIP_LIST:
  118. logger.info("Deleting packages: " + str(packageList) + "\n")
  119. self.do_erase_packages(packageList)
  120. if userList and not USER_SECTION in SKIP_LIST:
  121. logger.info("\n" + "Deleting users: " + str(userList))
  122. self.do_delete_users(userList)
  123. self.do_erase_dir_silent(homeDirList)
  124. self.do_delete_by_owner(userIds, FOLDER_LIST)
  125. if dirList and not DIR_SECTION in SKIP_LIST:
  126. logger.info("\n" + "Deleting directories: " + str(dirList))
  127. self.do_erase_dir_silent(dirList)
  128. if additionalDirList and not ADDITIONAL_DIRS in SKIP_LIST:
  129. logger.info("\n" + "Deleting additional directories: " + str(dirList))
  130. self.do_erase_dir_silent(additionalDirList)
  131. if repoList and not REPO_SECTION in SKIP_LIST:
  132. repoFiles = self.find_repo_files_for_repos(repoList)
  133. logger.info("\n" + "Deleting repo files: " + str(repoFiles))
  134. self.do_erase_files_silent(repoFiles)
  135. if alt_map and not ALT_SECTION in SKIP_LIST:
  136. logger.info("\n" + "Erasing alternatives:" + str(alt_map) + "\n")
  137. self.do_erase_alternatives(alt_map)
  138. return 0
  139. def read_host_check_file(self, config_file_path):
  140. propertyMap = {}
  141. try:
  142. with open(config_file_path, 'r'):
  143. pass
  144. except Exception, e:
  145. logger.error("Host check result not found at: " + str(config_file_path))
  146. return None
  147. try:
  148. config = ConfigParser.RawConfigParser()
  149. config.read(config_file_path)
  150. except Exception, e:
  151. logger.error("Cannot read host check result: " + str(e))
  152. return None
  153. # Initialize map from file
  154. try:
  155. if config.has_option(PACKAGE_SECTION, PACKAGE_KEY):
  156. propertyMap[PACKAGE_SECTION] = config.get(PACKAGE_SECTION, PACKAGE_KEY).split(',')
  157. except:
  158. logger.warn("Cannot read package list: " + str(sys.exc_info()[0]))
  159. try:
  160. if config.has_option(PROCESS_SECTION, PROCESS_KEY):
  161. propertyMap[PROCESS_SECTION] = config.get(PROCESS_SECTION, PROCESS_KEY).split(',')
  162. except:
  163. logger.warn("Cannot read process list: " + str(sys.exc_info()[0]))
  164. try:
  165. if config.has_option(USER_SECTION, USER_KEY):
  166. propertyMap[USER_SECTION] = config.get(USER_SECTION, USER_KEY).split(',')
  167. except:
  168. logger.warn("Cannot read user list: " + str(sys.exc_info()[0]))
  169. try:
  170. if config.has_option(USER_SECTION, USER_HOMEDIR_KEY):
  171. propertyMap[USER_HOMEDIR_SECTION] = config.get(USER_SECTION, USER_HOMEDIR_KEY).split(',')
  172. except:
  173. logger.warn("Cannot read user homedir list: " + str(sys.exc_info()[0]))
  174. try:
  175. if config.has_option(REPO_SECTION, REPOS_KEY):
  176. propertyMap[REPO_SECTION] = config.get(REPO_SECTION, REPOS_KEY).split(',')
  177. except:
  178. logger.warn("Cannot read repositories list: " + str(sys.exc_info()[0]))
  179. try:
  180. if config.has_option(DIR_SECTION, DIR_KEY):
  181. propertyMap[DIR_SECTION] = config.get(DIR_SECTION, DIR_KEY).split(',')
  182. except:
  183. logger.warn("Cannot read dir list: " + str(sys.exc_info()[0]))
  184. try:
  185. alt_map = {}
  186. if config.has_option(ALT_SECTION, ALT_KEYS[0]):
  187. alt_map[ALT_KEYS[0]] = config.get(ALT_SECTION, ALT_KEYS[0]).split(',')
  188. if config.has_option(ALT_SECTION, ALT_KEYS[1]):
  189. alt_map[ALT_KEYS[1]] = config.get(ALT_SECTION, ALT_KEYS[1]).split(',')
  190. if alt_map:
  191. propertyMap[ALT_SECTION] = alt_map
  192. except:
  193. logger.warn("Cannot read alternates list: " + str(sys.exc_info()[0]))
  194. return propertyMap
  195. def get_alternatives_desc(self, alt_name):
  196. command = ALT_DISP_CMD.format(alt_name)
  197. out = None
  198. try:
  199. p1 = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
  200. p2 = subprocess.Popen(["grep", "priority"], stdin=p1.stdout, stdout=subprocess.PIPE)
  201. p1.stdout.close()
  202. out = p2.communicate()[0]
  203. logger.debug('alternatives --display ' + alt_name + '\n, out = ' + out)
  204. except:
  205. logger.warn('Cannot process alternative named: ' + alt_name + ',' + \
  206. 'error: ' + str(sys.exc_info()[0]))
  207. return out
  208. def do_clear_cache(self, cache_root, dir_map=None):
  209. """
  210. Clear cache dir according to provided root directory
  211. cache_root - root dir for cache directory
  212. dir_map - should be used only for recursive calls
  213. """
  214. global CACHE_FILES_PATTERN
  215. file_map = CACHE_FILES_PATTERN if dir_map is None else dir_map
  216. remList = []
  217. # Build remove list according to masks
  218. for folder in file_map:
  219. if isinstance(file_map[folder], list): # here is list of file masks/files
  220. for mask in file_map[folder]:
  221. remList += self.get_files_in_dir("%s/%s" % (cache_root, folder), mask)
  222. elif isinstance(file_map[folder], dict): # here described sub-folder
  223. remList += self.do_clear_cache("%s/%s" % (cache_root, folder), file_map[folder])
  224. if dir_map is not None: # push result list back as this is call from stack
  225. return remList
  226. else: # root call, so we have final list
  227. self.do_erase_files_silent(remList)
  228. # Alternatives exist as a stack of symlinks under /var/lib/alternatives/$name
  229. # Script expects names of the alternatives as input
  230. # We find all the symlinks using command, #] alternatives --display $name
  231. # and delete them using command, #] alternatives --remove $name $path.
  232. def do_erase_alternatives(self, alt_map):
  233. if alt_map:
  234. alt_list = alt_map.get(ALT_KEYS[0])
  235. if alt_list:
  236. for alt_name in alt_list:
  237. if alt_name:
  238. out = self.get_alternatives_desc(alt_name)
  239. if not out:
  240. logger.warn('No alternatives found for: ' + alt_name)
  241. continue
  242. else:
  243. alternates = out.split('\n')
  244. if alternates:
  245. for entry in alternates:
  246. if entry:
  247. alt_path = entry.split()[0]
  248. logger.debug('Erasing alternative named: ' + alt_name + ', ' \
  249. 'path: ' + alt_path)
  250. command = ALT_ERASE_CMD.format(alt_name, alt_path)
  251. (returncode, stdoutdata, stderrdata) = self.run_os_command(command)
  252. if returncode != 0:
  253. logger.warn('Failed to remove alternative: ' + alt_name +
  254. ", path: " + alt_path + ", error: " + stderrdata)
  255. # Remove directories - configs
  256. dir_list = alt_map.get(ALT_KEYS[1])
  257. if dir_list:
  258. self.do_erase_dir_silent(dir_list)
  259. return 0
  260. def do_kill_processes(self, pidList):
  261. if pidList:
  262. for pid in pidList:
  263. if pid:
  264. command = PROC_KILL_CMD.format(pid)
  265. (returncode, stdoutdata, stderrdata) = self.run_os_command(command)
  266. if returncode != 0:
  267. logger.error("Unable to kill process with pid: " + pid + ", " + stderrdata)
  268. return 0
  269. def get_files_in_dir(self, dirPath, filemask = None):
  270. fileList = []
  271. if dirPath:
  272. if os.path.exists(dirPath):
  273. listdir = os.listdir(dirPath)
  274. if listdir:
  275. for link in listdir:
  276. path = dirPath + os.sep + link
  277. if not os.path.islink(path) and not os.path.isdir(path):
  278. if filemask is not None:
  279. if fnmatch.fnmatch(path, filemask):
  280. fileList.append(path)
  281. else:
  282. fileList.append(path)
  283. return fileList
  284. def find_repo_files_for_repos(self, repoNames):
  285. repoFiles = []
  286. osType = OSCheck.get_os_family()
  287. repoNameList = []
  288. for repoName in repoNames:
  289. if len(repoName.strip()) > 0:
  290. repoNameList.append("[" + repoName + "]")
  291. repoNameList.append("name=" + repoName)
  292. if repoNameList:
  293. # get list of files
  294. if osType == 'suse':
  295. fileList = self.get_files_in_dir(REPO_PATH_SUSE)
  296. elif osType == "redhat":
  297. fileList = self.get_files_in_dir(REPO_PATH_RHEL)
  298. else:
  299. logger.warn("Unsupported OS type, cannot get repository location.")
  300. return []
  301. if fileList:
  302. for filePath in fileList:
  303. with open(filePath, 'r') as file:
  304. content = file.readline()
  305. while (content != "" ):
  306. for repoName in repoNameList:
  307. if content.find(repoName) == 0 and filePath not in repoFiles:
  308. repoFiles.append(filePath)
  309. break;
  310. content = file.readline()
  311. return repoFiles
  312. def do_erase_packages(self, packageList):
  313. packageStr = None
  314. if packageList:
  315. packageStr = ' '.join(packageList)
  316. logger.debug("Erasing packages: " + packageStr)
  317. if packageStr is not None and packageStr:
  318. os_name = OSCheck.get_os_family()
  319. command = ''
  320. if os_name in PACKAGE_ERASE_CMD:
  321. command = PACKAGE_ERASE_CMD[os_name].format(packageStr)
  322. else:
  323. logger.warn("Unsupported OS type, cannot remove package.")
  324. if command != '':
  325. logger.debug('Executing: ' + str(command))
  326. (returncode, stdoutdata, stderrdata) = self.run_os_command(command)
  327. if returncode != 0:
  328. logger.warn("Erasing packages failed: " + stderrdata)
  329. else:
  330. logger.info("Erased packages successfully.\n" + stdoutdata)
  331. return 0
  332. def do_erase_dir_silent(self, pathList):
  333. if pathList:
  334. for path in pathList:
  335. if path and os.path.exists(path):
  336. if os.path.isdir(path):
  337. try:
  338. shutil.rmtree(path)
  339. except:
  340. logger.warn("Failed to remove dir: " + path + ", error: " + str(sys.exc_info()[0]))
  341. else:
  342. logger.info(path + " is a file and not a directory, deleting file")
  343. self.do_erase_files_silent([path])
  344. else:
  345. logger.info("Path doesn't exists: " + path)
  346. return 0
  347. def do_erase_files_silent(self, pathList):
  348. if pathList:
  349. for path in pathList:
  350. if path and os.path.exists(path):
  351. try:
  352. os.remove(path)
  353. except:
  354. logger.warn("Failed to delete file: " + path + ", error: " + str(sys.exc_info()[0]))
  355. else:
  356. logger.info("File doesn't exists: " + path)
  357. return 0
  358. def do_delete_group(self):
  359. groupDelCommand = GROUP_ERASE_CMD.format(HADOOP_GROUP)
  360. (returncode, stdoutdata, stderrdata) = self.run_os_command(groupDelCommand)
  361. if returncode != 0:
  362. logger.warn("Cannot delete group : " + HADOOP_GROUP + ", " + stderrdata)
  363. else:
  364. logger.info("Successfully deleted group: " + HADOOP_GROUP)
  365. def do_delete_by_owner(self, userIds, folders):
  366. for folder in folders:
  367. for filename in os.listdir(folder):
  368. fileToCheck = os.path.join(folder, filename)
  369. stat = os.stat(fileToCheck)
  370. if stat.st_uid in userIds:
  371. self.do_erase_dir_silent([fileToCheck])
  372. logger.info("Deleting file/folder: " + fileToCheck)
  373. def get_user_ids(self, userList):
  374. userIds = []
  375. if userList:
  376. for user in userList:
  377. if user:
  378. try:
  379. userIds.append(getpwnam(user).pw_uid)
  380. except Exception:
  381. logger.warn("Cannot find user : " + user)
  382. return userIds
  383. def do_delete_users(self, userList):
  384. if userList:
  385. for user in userList:
  386. if user:
  387. command = USER_ERASE_CMD.format(user)
  388. (returncode, stdoutdata, stderrdata) = self.run_os_command(command)
  389. if returncode != 0:
  390. logger.warn("Cannot delete user : " + user + ", " + stderrdata)
  391. else:
  392. logger.info("Successfully deleted user: " + user)
  393. self.do_delete_group()
  394. return 0
  395. def is_current_user_root(self):
  396. return os.getuid() == 0
  397. # Run command as sudoer by default, if root no issues
  398. def run_os_command(self, cmd, runWithSudo=True):
  399. if runWithSudo:
  400. cmd = 'sudo ' + cmd
  401. logger.info('Executing command: ' + str(cmd))
  402. if type(cmd) == str:
  403. cmd = shlex.split(cmd)
  404. process = subprocess.Popen(cmd,
  405. stdout=subprocess.PIPE,
  406. stdin=subprocess.PIPE,
  407. stderr=subprocess.PIPE
  408. )
  409. (stdoutdata, stderrdata) = process.communicate()
  410. return process.returncode, stdoutdata, stderrdata
  411. def search_file(self, filename, search_path, pathsep=os.pathsep):
  412. """ Given a search path, find file with requested name """
  413. for path in string.split(search_path, pathsep):
  414. candidate = os.path.join(path, filename)
  415. if os.path.exists(candidate): return os.path.abspath(candidate)
  416. return None
  417. # Copy file and save with file.# (timestamp)
  418. def backup_file(filePath):
  419. if filePath is not None and os.path.exists(filePath):
  420. timestamp = datetime.datetime.now()
  421. format = '%Y%m%d%H%M%S'
  422. try:
  423. shutil.copyfile(filePath, filePath + "." + timestamp.strftime(format))
  424. except (Exception), e:
  425. logger.warn('Could not backup file "%s": %s' % (str(filePath, e)))
  426. return 0
  427. def get_YN_input(prompt, default):
  428. yes = set(['yes', 'ye', 'y'])
  429. no = set(['no', 'n'])
  430. return get_choice_string_input(prompt, default, yes, no)
  431. def get_choice_string_input(prompt, default, firstChoice, secondChoice):
  432. choice = raw_input(prompt).lower()
  433. if choice in firstChoice:
  434. return True
  435. elif choice in secondChoice:
  436. return False
  437. elif choice is "": # Just enter pressed
  438. return default
  439. else:
  440. print "input not recognized, please try again: "
  441. return get_choice_string_input(prompt, default, firstChoice, secondChoice)
  442. pass
  443. def main():
  444. h = HostCleanup()
  445. config = h.resolve_ambari_config()
  446. hostCheckFileDir = config.get('agent', 'prefix')
  447. hostCheckFilePath = os.path.join(hostCheckFileDir, HOST_CHECK_FILE_NAME)
  448. hostCheckResultPath = os.path.join(hostCheckFileDir, OUTPUT_FILE_NAME)
  449. parser = optparse.OptionParser()
  450. parser.add_option("-v", "--verbose", dest="verbose", action="store_false",
  451. default=False, help="output verbosity.")
  452. parser.add_option("-f", "--file", dest="inputfile",
  453. default=hostCheckFilePath,
  454. help="host check result file to read.", metavar="FILE")
  455. parser.add_option("-o", "--out", dest="outputfile",
  456. default=hostCheckResultPath,
  457. help="log file to store results.", metavar="FILE")
  458. parser.add_option("-k", "--skip", dest="skip",
  459. help="(packages|users|directories|repositories|processes|alternatives)." + \
  460. " Use , as separator.")
  461. parser.add_option("-s", "--silent",
  462. action="store_true", dest="silent", default=False,
  463. help="Silently accepts default prompt values")
  464. (options, args) = parser.parse_args()
  465. # set output file
  466. backup_file(options.outputfile)
  467. global logger
  468. logger = logging.getLogger('HostCleanup')
  469. handler = logging.FileHandler(options.outputfile)
  470. formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
  471. handler.setFormatter(formatter)
  472. logger.addHandler(handler)
  473. # set verbose
  474. if options.verbose:
  475. logging.basicConfig(level=logging.DEBUG)
  476. else:
  477. logging.basicConfig(level=logging.INFO)
  478. if options.skip is not None:
  479. global SKIP_LIST
  480. SKIP_LIST = options.skip.split(',')
  481. is_root = h.is_current_user_root()
  482. if not is_root:
  483. raise RuntimeError('HostCleanup needs to be run as root.')
  484. if not options.silent:
  485. if "users" not in SKIP_LIST:
  486. delete_users = get_YN_input('You have elected to remove all users as well. If it is not intended then use '
  487. 'option --skip \"users\". Do you want to continue [y/n] (y)', True)
  488. if not delete_users:
  489. print 'Exiting. Use option --skip="users" to skip deleting users'
  490. sys.exit(1)
  491. hostcheckfile = options.inputfile
  492. propMap = h.read_host_check_file(hostcheckfile)
  493. if propMap:
  494. h.do_cleanup(propMap)
  495. if os.path.exists(config.get('agent', 'cache_dir')):
  496. h.do_clear_cache(config.get('agent', 'cache_dir'))
  497. logger.info('Clean-up completed. The output is at %s' % (str(options.outputfile)))
  498. if __name__ == '__main__':
  499. main()