test_pip_state.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.integration.states.pip_state
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. '''
  7. # Import python libs
  8. from __future__ import absolute_import, print_function, unicode_literals
  9. import errno
  10. import os
  11. import glob
  12. import shutil
  13. import sys
  14. try:
  15. import pwd
  16. HAS_PWD = True
  17. except ImportError:
  18. HAS_PWD = False
  19. # Import Salt Testing libs
  20. from tests.support.case import ModuleCase
  21. from tests.support.helpers import (
  22. destructiveTest,
  23. requires_system_grains,
  24. with_system_user,
  25. skip_if_not_root,
  26. with_tempdir
  27. )
  28. from tests.support.mixins import SaltReturnAssertsMixin
  29. from tests.support.runtests import RUNTIME_VARS
  30. from tests.support.unit import skipIf
  31. # Import salt libs
  32. import salt.utils.files
  33. import salt.utils.path
  34. import salt.utils.platform
  35. import salt.utils.versions
  36. import salt.utils.win_dacl
  37. import salt.utils.win_functions
  38. import salt.utils.win_runas
  39. from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES
  40. from salt.exceptions import CommandExecutionError
  41. # Import 3rd-party libs
  42. from salt.ext import six
  43. class VirtualEnv(object):
  44. def __init__(self, test, venv_dir):
  45. self.venv_dir = venv_dir
  46. self.test = test
  47. def __enter__(self):
  48. ret = self.test.run_function('virtualenv.create', [self.venv_dir])
  49. self.test.assertEqual(ret['retcode'], 0)
  50. def __exit__(self, exc_type, exc_value, traceback):
  51. if os.path.isdir(self.venv_dir):
  52. shutil.rmtree(self.venv_dir, ignore_errors=True)
  53. @skipIf(salt.utils.path.which_bin(KNOWN_BINARY_NAMES) is None, 'virtualenv not installed')
  54. class PipStateTest(ModuleCase, SaltReturnAssertsMixin):
  55. @skip_if_not_root
  56. def test_pip_installed_removed(self):
  57. '''
  58. Tests installed and removed states
  59. '''
  60. name = 'pudb'
  61. if name in self.run_function('pip.list'):
  62. self.skipTest('{0} is already installed, uninstall to run this test'.format(name))
  63. ret = self.run_state('pip.installed', name=name)
  64. self.assertSaltTrueReturn(ret)
  65. ret = self.run_state('pip.removed', name=name)
  66. self.assertSaltTrueReturn(ret)
  67. def test_pip_installed_removed_venv(self):
  68. venv_dir = os.path.join(
  69. RUNTIME_VARS.TMP, 'pip_installed_removed'
  70. )
  71. with VirtualEnv(self, venv_dir):
  72. name = 'pudb'
  73. ret = self.run_state('pip.installed', name=name, bin_env=venv_dir)
  74. self.assertSaltTrueReturn(ret)
  75. ret = self.run_state('pip.removed', name=name, bin_env=venv_dir)
  76. self.assertSaltTrueReturn(ret)
  77. def test_pip_installed_errors(self):
  78. venv_dir = os.path.join(
  79. RUNTIME_VARS.TMP, 'pip-installed-errors'
  80. )
  81. orig_shell = os.environ.get('SHELL')
  82. try:
  83. # Since we don't have the virtualenv created, pip.installed will
  84. # throw an error.
  85. # Example error strings:
  86. # * "Error installing 'pep8': /tmp/pip-installed-errors: not found"
  87. # * "Error installing 'pep8': /bin/sh: 1: /tmp/pip-installed-errors: not found"
  88. # * "Error installing 'pep8': /bin/bash: /tmp/pip-installed-errors: No such file or directory"
  89. os.environ['SHELL'] = '/bin/sh'
  90. ret = self.run_function('state.sls', mods='pip-installed-errors')
  91. self.assertSaltFalseReturn(ret)
  92. self.assertSaltCommentRegexpMatches(
  93. ret,
  94. 'Error installing \'pep8\':'
  95. )
  96. # We now create the missing virtualenv
  97. ret = self.run_function('virtualenv.create', [venv_dir])
  98. self.assertEqual(ret['retcode'], 0)
  99. # The state should not have any issues running now
  100. ret = self.run_function('state.sls', mods='pip-installed-errors')
  101. self.assertSaltTrueReturn(ret)
  102. finally:
  103. if orig_shell is None:
  104. # Didn't exist before, don't leave it there. This should never
  105. # happen, but if it does, we don't want this test to affect
  106. # others elsewhere in the suite.
  107. os.environ.pop('SHELL')
  108. else:
  109. os.environ['SHELL'] = orig_shell
  110. if os.path.isdir(venv_dir):
  111. shutil.rmtree(venv_dir, ignore_errors=True)
  112. @skipIf(six.PY3, 'Issue is specific to carbon module, which is PY2-only')
  113. @skipIf(salt.utils.platform.is_windows(), "Carbon does not install in Windows")
  114. @requires_system_grains
  115. def test_pip_installed_weird_install(self, grains=None):
  116. # First, check to see if this is running on CentOS 5 or MacOS.
  117. # If so, skip this test.
  118. if grains['os'] in ('CentOS',) and grains['osrelease_info'][0] in (5,):
  119. self.skipTest('This test does not run reliably on CentOS 5')
  120. if grains['os'] in ('MacOS',):
  121. self.skipTest('This test does not run reliably on MacOS')
  122. ographite = '/opt/graphite'
  123. if os.path.isdir(ographite):
  124. self.skipTest(
  125. 'You already have \'{0}\'. This test would overwrite this '
  126. 'directory'.format(ographite)
  127. )
  128. try:
  129. os.makedirs(ographite)
  130. except OSError as err:
  131. if err.errno == errno.EACCES:
  132. # Permission denied
  133. self.skipTest(
  134. 'You don\'t have the required permissions to run this test'
  135. )
  136. finally:
  137. if os.path.isdir(ographite):
  138. shutil.rmtree(ographite, ignore_errors=True)
  139. venv_dir = os.path.join(RUNTIME_VARS.TMP, 'pip-installed-weird-install')
  140. try:
  141. # We may be able to remove this, I had to add it because the custom
  142. # modules from the test suite weren't available in the jinja
  143. # context when running the call to state.sls that comes after.
  144. self.run_function('saltutil.sync_modules')
  145. # Since we don't have the virtualenv created, pip.installed will
  146. # throw an error.
  147. ret = self.run_function(
  148. 'state.sls', mods='pip-installed-weird-install'
  149. )
  150. self.assertSaltTrueReturn(ret)
  151. # We cannot use assertInSaltComment here because we need to skip
  152. # some of the state return parts
  153. for key in six.iterkeys(ret):
  154. self.assertTrue(ret[key]['result'])
  155. if ret[key]['name'] != 'carbon < 1.1':
  156. continue
  157. self.assertEqual(
  158. ret[key]['comment'],
  159. 'There was no error installing package \'carbon < 1.1\' '
  160. 'although it does not show when calling \'pip.freeze\'.'
  161. )
  162. break
  163. else:
  164. raise Exception('Expected state did not run')
  165. finally:
  166. if os.path.isdir(ographite):
  167. shutil.rmtree(ographite, ignore_errors=True)
  168. def test_issue_2028_pip_installed_state(self):
  169. ret = self.run_function('state.sls', mods='issue-2028-pip-installed')
  170. venv_dir = os.path.join(
  171. RUNTIME_VARS.TMP, 'issue-2028-pip-installed'
  172. )
  173. pep8_bin = os.path.join(venv_dir, 'bin', 'pep8')
  174. if salt.utils.platform.is_windows():
  175. pep8_bin = os.path.join(venv_dir, 'Scripts', 'pep8.exe')
  176. try:
  177. self.assertSaltTrueReturn(ret)
  178. self.assertTrue(
  179. os.path.isfile(pep8_bin)
  180. )
  181. finally:
  182. if os.path.isdir(venv_dir):
  183. shutil.rmtree(venv_dir, ignore_errors=True)
  184. def test_issue_2087_missing_pip(self):
  185. venv_dir = os.path.join(
  186. RUNTIME_VARS.TMP, 'issue-2087-missing-pip'
  187. )
  188. try:
  189. # Let's create the testing virtualenv
  190. ret = self.run_function('virtualenv.create', [venv_dir])
  191. self.assertEqual(ret['retcode'], 0)
  192. # Let's remove the pip binary
  193. pip_bin = os.path.join(venv_dir, 'bin', 'pip')
  194. site_dir = self.run_function('virtualenv.get_distribution_path', [venv_dir, 'pip'])
  195. if salt.utils.platform.is_windows():
  196. pip_bin = os.path.join(venv_dir, 'Scripts', 'pip.exe')
  197. site_dir = os.path.join(venv_dir, 'lib', 'site-packages')
  198. if not os.path.isfile(pip_bin):
  199. self.skipTest(
  200. 'Failed to find the pip binary to the test virtualenv'
  201. )
  202. os.remove(pip_bin)
  203. # Also remove the pip dir from site-packages
  204. # This is needed now that we're using python -m pip instead of the
  205. # pip binary directly. python -m pip will still work even if the
  206. # pip binary is missing
  207. shutil.rmtree(os.path.join(site_dir, 'pip'))
  208. # Let's run the state which should fail because pip is missing
  209. ret = self.run_function('state.sls', mods='issue-2087-missing-pip')
  210. self.assertSaltFalseReturn(ret)
  211. self.assertInSaltComment(
  212. 'Error installing \'pep8\': Could not find a `pip` binary',
  213. ret
  214. )
  215. finally:
  216. if os.path.isdir(venv_dir):
  217. shutil.rmtree(venv_dir, ignore_errors=True)
  218. def test_issue_5940_multiple_pip_mirrors(self):
  219. '''
  220. Test multiple pip mirrors. This test only works with pip < 7.0.0
  221. '''
  222. ret = self.run_function(
  223. 'state.sls', mods='issue-5940-multiple-pip-mirrors'
  224. )
  225. venv_dir = os.path.join(
  226. RUNTIME_VARS.TMP, '5940-multiple-pip-mirrors'
  227. )
  228. try:
  229. self.assertSaltTrueReturn(ret)
  230. self.assertTrue(
  231. os.path.isfile(os.path.join(venv_dir, 'bin', 'pep8'))
  232. )
  233. except (AssertionError, CommandExecutionError):
  234. pip_version = self.run_function('pip.version', [venv_dir])
  235. if salt.utils.versions.compare(ver1=pip_version, oper='>=', ver2='7.0.0'):
  236. self.skipTest('the --mirrors arg has been deprecated and removed in pip==7.0.0')
  237. finally:
  238. if os.path.isdir(venv_dir):
  239. shutil.rmtree(venv_dir, ignore_errors=True)
  240. @destructiveTest
  241. @skip_if_not_root
  242. @with_system_user('issue-6912', on_existing='delete', delete=True,
  243. password='PassWord1!')
  244. @with_tempdir()
  245. def test_issue_6912_wrong_owner(self, temp_dir, username):
  246. # Setup virtual environment directory to be used throughout the test
  247. venv_dir = os.path.join(temp_dir, '6912-wrong-owner')
  248. # The virtual environment needs to be in a location that is accessible
  249. # by both the user running the test and the runas user
  250. if salt.utils.platform.is_windows():
  251. salt.utils.win_dacl.set_permissions(temp_dir, username, 'full_control')
  252. else:
  253. uid = self.run_function('file.user_to_uid', [username])
  254. os.chown(temp_dir, uid, -1)
  255. # Create the virtual environment
  256. venv_create = self.run_function(
  257. 'virtualenv.create', [venv_dir], user=username,
  258. password='PassWord1!')
  259. if venv_create['retcode'] > 0:
  260. self.skipTest('Failed to create testcase virtual environment: {0}'
  261. ''.format(venv_create))
  262. # pip install passing the package name in `name`
  263. ret = self.run_state(
  264. 'pip.installed', name='pep8', user=username, bin_env=venv_dir,
  265. password='PassWord1!')
  266. self.assertSaltTrueReturn(ret)
  267. if HAS_PWD:
  268. uid = pwd.getpwnam(username).pw_uid
  269. for globmatch in (os.path.join(venv_dir, '**', 'pep8*'),
  270. os.path.join(venv_dir, '*', '**', 'pep8*'),
  271. os.path.join(venv_dir, '*', '*', '**', 'pep8*')):
  272. for path in glob.glob(globmatch):
  273. if HAS_PWD:
  274. self.assertEqual(uid, os.stat(path).st_uid)
  275. elif salt.utils.platform.is_windows():
  276. self.assertEqual(
  277. salt.utils.win_dacl.get_owner(path), username)
  278. @destructiveTest
  279. @skip_if_not_root
  280. @skipIf(salt.utils.platform.is_darwin(), 'Test is flaky on macosx')
  281. @with_system_user('issue-6912', on_existing='delete', delete=True,
  282. password='PassWord1!')
  283. @with_tempdir()
  284. def test_issue_6912_wrong_owner_requirements_file(self, temp_dir, username):
  285. # Setup virtual environment directory to be used throughout the test
  286. venv_dir = os.path.join(temp_dir, '6912-wrong-owner')
  287. # The virtual environment needs to be in a location that is accessible
  288. # by both the user running the test and the runas user
  289. if salt.utils.platform.is_windows():
  290. salt.utils.win_dacl.set_permissions(temp_dir, username, 'full_control')
  291. else:
  292. uid = self.run_function('file.user_to_uid', [username])
  293. os.chown(temp_dir, uid, -1)
  294. # Create the virtual environment again as it should have been removed
  295. venv_create = self.run_function(
  296. 'virtualenv.create', [venv_dir], user=username,
  297. password='PassWord1!')
  298. if venv_create['retcode'] > 0:
  299. self.skipTest('failed to create testcase virtual environment: {0}'
  300. ''.format(venv_create))
  301. # pip install using a requirements file
  302. req_filename = os.path.join(
  303. RUNTIME_VARS.TMP_STATE_TREE, 'issue-6912-requirements.txt'
  304. )
  305. with salt.utils.files.fopen(req_filename, 'wb') as reqf:
  306. reqf.write(b'pep8\n')
  307. ret = self.run_state(
  308. 'pip.installed', name='', user=username, bin_env=venv_dir,
  309. requirements='salt://issue-6912-requirements.txt',
  310. password='PassWord1!')
  311. self.assertSaltTrueReturn(ret)
  312. if HAS_PWD:
  313. uid = pwd.getpwnam(username).pw_uid
  314. for globmatch in (os.path.join(venv_dir, '**', 'pep8*'),
  315. os.path.join(venv_dir, '*', '**', 'pep8*'),
  316. os.path.join(venv_dir, '*', '*', '**', 'pep8*')):
  317. for path in glob.glob(globmatch):
  318. if HAS_PWD:
  319. self.assertEqual(uid, os.stat(path).st_uid)
  320. elif salt.utils.platform.is_windows():
  321. self.assertEqual(
  322. salt.utils.win_dacl.get_owner(path), username)
  323. def test_issue_6833_pip_upgrade_pip(self):
  324. # Create the testing virtualenv
  325. venv_dir = os.path.join(
  326. RUNTIME_VARS.TMP, '6833-pip-upgrade-pip'
  327. )
  328. ret = self.run_function('virtualenv.create', [venv_dir])
  329. try:
  330. try:
  331. self.assertEqual(ret['retcode'], 0)
  332. self.assertIn(
  333. 'New python executable',
  334. ret['stdout']
  335. )
  336. except AssertionError:
  337. import pprint
  338. pprint.pprint(ret)
  339. raise
  340. # Let's install a fixed version pip over whatever pip was
  341. # previously installed
  342. ret = self.run_function(
  343. 'pip.install', ['pip==8.0'], upgrade=True,
  344. bin_env=venv_dir
  345. )
  346. try:
  347. self.assertEqual(ret['retcode'], 0)
  348. self.assertIn(
  349. 'Successfully installed pip',
  350. ret['stdout']
  351. )
  352. except AssertionError:
  353. import pprint
  354. pprint.pprint(ret)
  355. raise
  356. # Let's make sure we have pip 8.0 installed
  357. self.assertEqual(
  358. self.run_function('pip.list', ['pip'], bin_env=venv_dir),
  359. {'pip': '8.0.0'}
  360. )
  361. # Now the actual pip upgrade pip test
  362. ret = self.run_state(
  363. 'pip.installed', name='pip==8.0.1', upgrade=True,
  364. bin_env=venv_dir
  365. )
  366. try:
  367. self.assertSaltTrueReturn(ret)
  368. self.assertSaltStateChangesEqual(
  369. ret, {'pip==8.0.1': 'Installed'})
  370. except AssertionError:
  371. import pprint
  372. pprint.pprint(ret)
  373. raise
  374. finally:
  375. if os.path.isdir(venv_dir):
  376. shutil.rmtree(venv_dir, ignore_errors=True)
  377. def test_pip_installed_specific_env(self):
  378. # Create the testing virtualenv
  379. venv_dir = os.path.join(
  380. RUNTIME_VARS.TMP, 'pip-installed-specific-env'
  381. )
  382. # Let's write a requirements file
  383. requirements_file = os.path.join(
  384. RUNTIME_VARS.TMP_PRODENV_STATE_TREE, 'prod-env-requirements.txt'
  385. )
  386. with salt.utils.files.fopen(requirements_file, 'wb') as reqf:
  387. reqf.write(b'pep8\n')
  388. try:
  389. self.run_function('virtualenv.create', [venv_dir])
  390. # The requirements file should not be found the base environment
  391. ret = self.run_state(
  392. 'pip.installed', name='', bin_env=venv_dir,
  393. requirements='salt://prod-env-requirements.txt'
  394. )
  395. self.assertSaltFalseReturn(ret)
  396. self.assertInSaltComment(
  397. "'salt://prod-env-requirements.txt' not found", ret
  398. )
  399. # The requirements file must be found in the prod environment
  400. ret = self.run_state(
  401. 'pip.installed', name='', bin_env=venv_dir, saltenv='prod',
  402. requirements='salt://prod-env-requirements.txt'
  403. )
  404. self.assertSaltTrueReturn(ret)
  405. self.assertInSaltComment(
  406. 'Successfully processed requirements file '
  407. 'salt://prod-env-requirements.txt', ret
  408. )
  409. # We're using the base environment but we're passing the prod
  410. # environment as an url arg to salt://
  411. ret = self.run_state(
  412. 'pip.installed', name='', bin_env=venv_dir,
  413. requirements='salt://prod-env-requirements.txt?saltenv=prod'
  414. )
  415. self.assertSaltTrueReturn(ret)
  416. self.assertInSaltComment(
  417. 'Requirements were already installed.',
  418. ret
  419. )
  420. finally:
  421. if os.path.isdir(venv_dir):
  422. shutil.rmtree(venv_dir, ignore_errors=True)
  423. if os.path.isfile(requirements_file):
  424. os.unlink(requirements_file)
  425. def test_22359_pip_installed_unless_does_not_trigger_warnings(self):
  426. # This test case should be moved to a format_call unit test specific to
  427. # the state internal keywords
  428. venv_dir = os.path.join(RUNTIME_VARS.TMP, 'pip-installed-unless')
  429. venv_create = self.run_function('virtualenv.create', [venv_dir])
  430. if venv_create['retcode'] > 0:
  431. self.skipTest(
  432. 'Failed to create testcase virtual environment: {0}'.format(
  433. venv_create
  434. )
  435. )
  436. false_cmd = '/bin/false'
  437. if salt.utils.platform.is_windows():
  438. false_cmd = 'exit 1 >nul'
  439. try:
  440. ret = self.run_state(
  441. 'pip.installed', name='pep8', bin_env=venv_dir, unless=false_cmd
  442. )
  443. self.assertSaltTrueReturn(ret)
  444. self.assertNotIn('warnings', next(six.itervalues(ret)))
  445. finally:
  446. if os.path.isdir(venv_dir):
  447. shutil.rmtree(venv_dir, ignore_errors=True)
  448. @skipIf(sys.version_info[:2] >= (3, 6), 'Old version of virtualenv too old for python3.6')
  449. @skipIf(salt.utils.platform.is_windows(), "Carbon does not install in Windows")
  450. def test_46127_pip_env_vars(self):
  451. '''
  452. Test that checks if env_vars passed to pip.installed are also passed
  453. to pip.freeze while checking for existing installations
  454. '''
  455. # This issue is most easily checked while installing carbon
  456. # Much of the code here comes from the test_weird_install function above
  457. ographite = '/opt/graphite'
  458. if os.path.isdir(ographite):
  459. self.skipTest(
  460. 'You already have \'{0}\'. This test would overwrite this '
  461. 'directory'.format(ographite)
  462. )
  463. try:
  464. os.makedirs(ographite)
  465. except OSError as err:
  466. if err.errno == errno.EACCES:
  467. # Permission denied
  468. self.skipTest(
  469. 'You don\'t have the required permissions to run this test'
  470. )
  471. finally:
  472. if os.path.isdir(ographite):
  473. shutil.rmtree(ographite, ignore_errors=True)
  474. venv_dir = os.path.join(RUNTIME_VARS.TMP, 'issue-46127-pip-env-vars')
  475. try:
  476. # We may be able to remove this, I had to add it because the custom
  477. # modules from the test suite weren't available in the jinja
  478. # context when running the call to state.sls that comes after.
  479. self.run_function('saltutil.sync_modules')
  480. # Since we don't have the virtualenv created, pip.installed will
  481. # throw an error.
  482. ret = self.run_function(
  483. 'state.sls', mods='issue-46127-pip-env-vars'
  484. )
  485. self.assertSaltTrueReturn(ret)
  486. for key in six.iterkeys(ret):
  487. self.assertTrue(ret[key]['result'])
  488. if ret[key]['name'] != 'carbon < 1.3':
  489. continue
  490. self.assertEqual(
  491. ret[key]['comment'],
  492. 'All packages were successfully installed'
  493. )
  494. break
  495. else:
  496. raise Exception('Expected state did not run')
  497. # Run the state again. Now the already installed message should
  498. # appear
  499. ret = self.run_function(
  500. 'state.sls', mods='issue-46127-pip-env-vars'
  501. )
  502. self.assertSaltTrueReturn(ret)
  503. # We cannot use assertInSaltComment here because we need to skip
  504. # some of the state return parts
  505. for key in six.iterkeys(ret):
  506. self.assertTrue(ret[key]['result'])
  507. # As we are re-running the formula, some states will not be run
  508. # and "name" may or may not be present, so we use .get() pattern
  509. if ret[key].get('name', '') != 'carbon < 1.3':
  510. continue
  511. self.assertEqual(
  512. ret[key]['comment'],
  513. ('All packages were successfully installed'))
  514. break
  515. else:
  516. raise Exception('Expected state did not run')
  517. finally:
  518. if os.path.isdir(ographite):
  519. shutil.rmtree(ographite, ignore_errors=True)
  520. if os.path.isdir(venv_dir):
  521. shutil.rmtree(venv_dir)