PageRenderTime 57ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/salt/utils/verify.py

https://github.com/patrickw-nf/salt
Python | 390 lines | 291 code | 35 blank | 64 comment | 88 complexity | 5d596cc4ab0b1d369859d56c3b3dca9a MD5 | raw file
Possible License(s): Apache-2.0
  1. '''
  2. A few checks to make sure the environment is sane
  3. '''
  4. # Original Author: Jeff Schroeder <jeffschroeder@computer.org>
  5. # Import python libs
  6. import os
  7. import re
  8. import sys
  9. import stat
  10. import socket
  11. import getpass
  12. import logging
  13. # Import third party libs
  14. if sys.platform.startswith('win'):
  15. import win32file
  16. else:
  17. import resource
  18. # Import salt libs
  19. from salt.log import is_console_configured
  20. from salt.exceptions import SaltClientError
  21. import salt.utils
  22. log = logging.getLogger(__name__)
  23. def zmq_version():
  24. '''
  25. ZeroMQ python bindings >= 2.1.9 are required
  26. '''
  27. import zmq
  28. ver = zmq.__version__
  29. # The last matched group can be None if the version
  30. # is something like 3.1 and that will work properly
  31. match = re.match('^(\d+)\.(\d+)(?:\.(\d+))?', ver)
  32. # Fallthrough and hope for the best
  33. if not match:
  34. msg = "Using untested zmq python bindings version: '{0}'".format(ver)
  35. if is_console_configured():
  36. log.warn(msg)
  37. else:
  38. sys.stderr.write("WARNING {0}\n".format(msg))
  39. return True
  40. major, minor, point = match.groups()
  41. if major.isdigit():
  42. major = int(major)
  43. if minor.isdigit():
  44. minor = int(minor)
  45. # point very well could be None
  46. if point and point.isdigit():
  47. point = int(point)
  48. if major == 2 and minor == 1:
  49. # zmq 2.1dev could be built against a newer libzmq
  50. if "dev" in ver and not point:
  51. msg = 'Using dev zmq module, please report unexpected results'
  52. if is_console_configured():
  53. log.warn(msg)
  54. else:
  55. sys.stderr.write("WARNING: {0}\n".format(msg))
  56. return True
  57. elif point and point >= 9:
  58. return True
  59. elif major > 2 or (major == 2 and minor > 1):
  60. return True
  61. # If all else fails, gracefully croak and warn the user
  62. log.critical('ZeroMQ python bindings >= 2.1.9 are required')
  63. if 'salt-master' in sys.argv[0]:
  64. msg = ('The Salt Master is unstable using a ZeroMQ version '
  65. 'lower than 2.1.11 and requires this fix: http://lists.zeromq.'
  66. 'org/pipermail/zeromq-dev/2011-June/012094.html')
  67. if is_console_configured():
  68. log.critical(msg)
  69. else:
  70. sys.stderr.write("CRITICAL {0}\n".format(msg))
  71. return False
  72. def verify_socket(interface, pub_port, ret_port):
  73. '''
  74. Attempt to bind to the sockets to verify that they are available
  75. '''
  76. pubsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  77. retsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  78. try:
  79. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  80. pubsock.bind((interface, int(pub_port)))
  81. pubsock.close()
  82. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  83. retsock.bind((interface, int(ret_port)))
  84. retsock.close()
  85. result = True
  86. except Exception:
  87. msg = ("Unable to bind socket, this might not be a problem."
  88. " Is there another salt-master running?")
  89. if is_console_configured():
  90. log.warn(msg)
  91. else:
  92. sys.stderr.write("WARNING: {0}\n".format(msg))
  93. result = False
  94. finally:
  95. pubsock.close()
  96. retsock.close()
  97. return result
  98. def verify_files(files, user):
  99. '''
  100. Verify that the named files exist and are owned by the named user
  101. '''
  102. if 'os' in os.environ:
  103. if os.environ['os'].startswith('Windows'):
  104. return True
  105. import pwd # after confirming not running Windows
  106. import grp
  107. try:
  108. pwnam = pwd.getpwnam(user)
  109. uid = pwnam[2]
  110. gid = pwnam[3]
  111. groups = [g.gr_gid for g in grp.getgrall() if user in g.gr_mem]
  112. except KeyError:
  113. err = ('Failed to prepare the Salt environment for user '
  114. '{0}. The user is not available.\n').format(user)
  115. sys.stderr.write(err)
  116. sys.exit(2)
  117. for fn_ in files:
  118. dirname = os.path.dirname(fn_)
  119. if not os.path.isdir(dirname):
  120. os.makedirs(dirname)
  121. if not os.path.isfile(fn_):
  122. with salt.utils.fopen(fn_, 'w+') as fp_:
  123. fp_.write('')
  124. stats = os.stat(fn_)
  125. if not uid == stats.st_uid:
  126. try:
  127. os.chown(fn_, uid, -1)
  128. except OSError:
  129. pass
  130. return True
  131. def verify_env(dirs, user, permissive=False, pki_dir=''):
  132. '''
  133. Verify that the named directories are in place and that the environment
  134. can shake the salt
  135. '''
  136. if 'os' in os.environ:
  137. if os.environ['os'].startswith('Windows'):
  138. return True
  139. import pwd # after confirming not running Windows
  140. import grp
  141. try:
  142. pwnam = pwd.getpwnam(user)
  143. uid = pwnam[2]
  144. gid = pwnam[3]
  145. groups = [g.gr_gid for g in grp.getgrall() if user in g.gr_mem]
  146. except KeyError:
  147. err = ('Failed to prepare the Salt environment for user '
  148. '{0}. The user is not available.\n').format(user)
  149. sys.stderr.write(err)
  150. sys.exit(2)
  151. for dir_ in dirs:
  152. if not dir_:
  153. continue
  154. if not os.path.isdir(dir_):
  155. try:
  156. cumask = os.umask(18) # 077
  157. os.makedirs(dir_)
  158. # If starting the process as root, chown the new dirs
  159. if os.getuid() == 0:
  160. os.chown(dir_, uid, gid)
  161. os.umask(cumask)
  162. except OSError as err:
  163. msg = 'Failed to create directory path "{0}" - {1}\n'
  164. sys.stderr.write(msg.format(dir_, err))
  165. sys.exit(err.errno)
  166. mode = os.stat(dir_)
  167. # If starting the process as root, chown the new dirs
  168. if os.getuid() == 0:
  169. fmode = os.stat(dir_)
  170. if not fmode.st_uid == uid or not fmode.st_gid == gid:
  171. if permissive and fmode.st_gid in groups:
  172. # Allow the directory to be owned by any group root
  173. # belongs to if we say it's ok to be permissive
  174. pass
  175. else:
  176. # chown the file for the new user
  177. os.chown(dir_, uid, gid)
  178. for root, dirs, files in os.walk(dir_):
  179. if 'jobs' in root:
  180. continue
  181. for name in files:
  182. if name.startswith('.'):
  183. continue
  184. path = os.path.join(root, name)
  185. try:
  186. fmode = os.stat(path)
  187. except (IOError, OSError):
  188. pass
  189. if not fmode.st_uid == uid or not fmode.st_gid == gid:
  190. if permissive and fmode.st_gid in groups:
  191. pass
  192. else:
  193. # chown the file for the new user
  194. os.chown(path, uid, gid)
  195. for name in dirs:
  196. path = os.path.join(root, name)
  197. fmode = os.stat(path)
  198. if not fmode.st_uid == uid or not fmode.st_gid == gid:
  199. if permissive and fmode.st_gid in groups:
  200. pass
  201. else:
  202. # chown the file for the new user
  203. os.chown(path, uid, gid)
  204. # Allow the pki dir to be 700 or 750, but nothing else.
  205. # This prevents other users from writing out keys, while
  206. # allowing the use-case of 3rd-party software (like django)
  207. # to read in what it needs to integrate.
  208. #
  209. # If the permissions aren't correct, default to the more secure 700.
  210. # If acls are enabled, the pki_dir needs to remain readable, this
  211. # is still secure because the private keys are still only readbale
  212. # by the user running the master
  213. if dir_ == pki_dir:
  214. smode = stat.S_IMODE(mode.st_mode)
  215. if not smode == 448 and not smode == 488:
  216. if os.access(dir_, os.W_OK):
  217. os.chmod(dir_, 448)
  218. else:
  219. msg = 'Unable to securely set the permissions of "{0}".'
  220. msg = msg.format(dir_)
  221. if is_console_configured():
  222. log.critical(msg)
  223. else:
  224. sys.stderr.write("CRITICAL: {0}\n".format(msg))
  225. # Run the extra verification checks
  226. zmq_version()
  227. def check_user(user):
  228. '''
  229. Check user and assign process uid/gid.
  230. '''
  231. if 'os' in os.environ:
  232. if os.environ['os'].startswith('Windows'):
  233. return True
  234. if user == getpass.getuser():
  235. return True
  236. import pwd # after confirming not running Windows
  237. try:
  238. pwuser = pwd.getpwnam(user)
  239. try:
  240. os.setgid(pwuser.pw_gid)
  241. os.setuid(pwuser.pw_uid)
  242. except OSError:
  243. msg = 'Salt configured to run as user "{0}" but unable to switch.'
  244. msg = msg.format(user)
  245. if is_console_configured():
  246. log.critical(msg)
  247. else:
  248. sys.stderr.write("CRITICAL: {0}\n".format(msg))
  249. return False
  250. except KeyError:
  251. msg = 'User not found: "{0}"'.format(user)
  252. if is_console_configured():
  253. log.critical(msg)
  254. else:
  255. sys.stderr.write("CRITICAL: {0}\n".format(msg))
  256. return False
  257. return True
  258. def list_path_traversal(path):
  259. """
  260. Returns a full list of directories leading up to, and including, a path.
  261. So list_path_traversal('/path/to/salt') would return:
  262. ['/', '/path', '/path/to', '/path/to/salt']
  263. in that order.
  264. This routine has been tested on Windows systems as well.
  265. list_path_traversal('c:\\path\\to\\salt') on Windows would return:
  266. ['c:\\', 'c:\\path', 'c:\\path\\to', 'c:\\path\\to\\salt']
  267. """
  268. out = [path]
  269. (head, tail) = os.path.split(path)
  270. if tail == '':
  271. # paths with trailing separators will return an empty string
  272. out = [head]
  273. (head, tail) = os.path.split(head)
  274. while head != out[0]:
  275. # loop until head is the same two consecutive times
  276. out.insert(0, head)
  277. (head, tail) = os.path.split(head)
  278. return out
  279. def check_path_traversal(path, user='root'):
  280. '''
  281. Walk from the root up to a directory and verify that the current
  282. user has access to read each directory. This is used for making
  283. sure a user can read all parent directories of the minion's key
  284. before trying to go and generate a new key and raising an IOError
  285. '''
  286. for tpath in list_path_traversal(path):
  287. if not os.access(tpath, os.R_OK):
  288. msg = 'Could not access {0}.'.format(tpath)
  289. current_user = getpass.getuser()
  290. # Make the error message more intelligent based on how
  291. # the user invokes salt-call or whatever other script.
  292. if user != current_user:
  293. msg += ' Try running as user {0}.'.format(user)
  294. else:
  295. msg += ' Please give {0} read permissions.'.format(user, tpath)
  296. # Propagate this exception up so there isn't a sys.exit()
  297. # in the middle of code that could be imported elsewhere.
  298. raise SaltClientError(msg)
  299. def check_max_open_files(opts):
  300. mof_c = opts.get('max_open_files', 100000)
  301. if sys.platform.startswith('win'):
  302. # Check the windows api for more detail on this
  303. # http://msdn.microsoft.com/en-us/library/xt874334(v=vs.71).aspx
  304. # and the python binding http://timgolden.me.uk/pywin32-docs/win32file.html
  305. mof_s = mof_h = win32file._getmaxstdio()
  306. else:
  307. mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE)
  308. accepted_keys_dir = os.path.join(opts.get('pki_dir'), 'minions')
  309. accepted_count = len([
  310. key for key in os.listdir(accepted_keys_dir) if
  311. os.path.isfile(os.path.join(accepted_keys_dir, key))
  312. ])
  313. log.debug(
  314. 'This salt-master instance has accepted {0} minion keys.'.format(
  315. accepted_count
  316. )
  317. )
  318. level = logging.INFO
  319. if (accepted_count * 4) <= mof_s:
  320. # We check for the soft value of max open files here because that's the
  321. # value the user chose to raise to.
  322. #
  323. # The number of accepted keys multiplied by four(4) is lower than the
  324. # soft value, everything should be OK
  325. return
  326. msg = (
  327. 'The number of accepted minion keys({0}) should be lower than 1/4 '
  328. 'of the max open files soft setting({1}). '.format(
  329. accepted_count, mof_s
  330. )
  331. )
  332. if accepted_count >= mof_s:
  333. # This should never occur, it might have already crashed
  334. msg += 'salt-master will crash pretty soon! '
  335. level = logging.CRITICAL
  336. elif (accepted_count * 2) >= mof_s:
  337. # This is way too low, CRITICAL
  338. level = logging.CRITICAL
  339. elif (accepted_count * 3) >= mof_s:
  340. level = logging.WARNING
  341. # The accepted count is more than 3 time, WARN
  342. elif (accepted_count * 4) >= mof_s:
  343. level = logging.INFO
  344. if mof_c < mof_h:
  345. msg += ('According to the system\'s hard limit, there\'s still a '
  346. 'margin of {0} to raise the salt\'s max_open_files '
  347. 'setting. ').format(mof_h - mof_c)
  348. msg += 'Please consider raising this value.'
  349. log.log(level=level, msg=msg)