Backport of PR #21358 to 2014.7

This commit is contained in:
Ross Perkins 2015-03-10 09:38:16 -07:00
parent f6f6afef76
commit 8083cf554d
3 changed files with 597 additions and 82 deletions

View file

@ -2,13 +2,15 @@
'''
Use composer to install PHP dependencies for a directory
'''
from __future__ import absolute_import
# Import python libs
import logging
import os.path
# Import salt libs
import salt.utils
from salt.exceptions import CommandExecutionError
from salt.exceptions import CommandExecutionError, CommandNotFoundError, SaltInvocationError
log = logging.getLogger(__name__)
@ -34,6 +36,161 @@ def _valid_composer(composer):
return False
def did_composer_install(dir):
'''
Test to see if the composer.lock file exists in this directory
dir
Directory location of the composer.json file
CLI Example:
.. code-block:: bash
salt '*' composer.did_composer_install /var/www/application
'''
lockFile = "{0}/composer.lock".format(dir)
if os.path.exists(lockFile):
return True
return False
def _run_composer(action,
dir=None,
composer=None,
php=None,
runas=None,
prefer_source=None,
prefer_dist=None,
no_scripts=None,
no_plugins=None,
optimize=None,
no_dev=None,
quiet=False,
composer_home='/root'):
'''
Run PHP's composer with a specific action.
If composer has not been installed globally making it available in the
system PATH & making it executable, the ``composer`` and ``php`` parameters
will need to be set to the location of the executables.
action
The action to pass to composer ('install', 'update', 'selfupdate', etc).
dir
Directory location of the composer.json file. Required except when
action='selfupdate'
composer
Location of the composer.phar file. If not set composer will
just execute "composer" as if it is installed globally.
(i.e. /path/to/composer.phar)
php
Location of the php executable to use with composer.
(i.e. /usr/bin/php)
runas
Which system user to run composer as.
prefer_source
--prefer-source option of composer.
prefer_dist
--prefer-dist option of composer.
no_scripts
--no-scripts option of composer.
no_plugins
--no-plugins option of composer.
optimize
--optimize-autoloader option of composer. Recommended for production.
no_dev
--no-dev option for composer. Recommended for production.
quiet
--quiet option for composer. Whether or not to return output from composer.
composer_home
$COMPOSER_HOME environment variable
'''
if composer is not None:
if php is None:
php = 'php'
else:
composer = 'composer'
# Validate Composer is there
if not _valid_composer(composer):
raise CommandNotFoundError('\'composer.{0}\' is not available. Couldn\'t find {1!r}.'
.format(action, composer))
# Don't need a dir for the 'selfupdate' action; all other actions do need a dir
if dir is None and action != 'selfupdate':
raise SaltInvocationError('{0!r} is required for \'composer.{1}\''
.format('dir', action))
if action is None:
raise SaltInvocationError('{0!r} is required for {1!r}'
.format('action', 'composer._run_composer'))
# If we're running an update, and if composer.lock does not exist, then
# we really need to run install instead.
if action == 'update':
if not did_composer_install(dir):
action = 'install'
# Base Settings
cmd = '{0} {1} {2}'.format(composer, action, '--no-interaction --no-ansi')
# If php is set, prepend it
if php is not None:
cmd = php + ' ' + cmd
# Add Working Dir
if dir is not None:
cmd += ' --working-dir=' + dir
# Other Settings
if quiet is True:
cmd += ' --quiet'
if no_dev is True:
cmd += ' --no-dev'
if prefer_source is True:
cmd += ' --prefer-source'
if prefer_dist is True:
cmd += ' --prefer-dist'
if no_scripts is True:
cmd += ' --no-scripts'
if no_plugins is True:
cmd += ' --no-plugins'
if optimize is True:
cmd += ' --optimize-autoloader'
result = __salt__['cmd.run_all'](cmd,
runas=runas,
env={'COMPOSER_HOME': composer_home},
python_shell=False)
if result['retcode'] != 0:
raise CommandExecutionError(result['stderr'])
if quiet is True:
return True
return result
def install(dir,
composer=None,
php=None,
@ -101,60 +258,148 @@ def install(dir,
salt '*' composer.install /var/www/application \
no_dev=True optimize=True
'''
if composer is not None:
if php is None:
php = 'php'
else:
composer = 'composer'
result = _run_composer('install',
dir=dir,
composer=composer,
php=php,
runas=runas,
prefer_source=prefer_source,
prefer_dist=prefer_dist,
no_scripts=no_scripts,
no_plugins=no_plugins,
optimize=optimize,
no_dev=no_dev,
quiet=quiet,
composer_home=composer_home)
return result
# Validate Composer is there
if not _valid_composer(composer):
return '{0!r} is not available. Couldn\'t find {1!r}.'.format('composer.install', composer)
if dir is None:
return '{0!r} is required for {1!r}'.format('dir', 'composer.install')
def update(dir,
composer=None,
php=None,
runas=None,
prefer_source=None,
prefer_dist=None,
no_scripts=None,
no_plugins=None,
optimize=None,
no_dev=None,
quiet=False,
composer_home='/root'):
'''
Update composer dependencies for a directory.
# Base Settings
cmd = composer + ' install --no-interaction'
If `composer install` has not yet been run, this runs `composer install`
instead.
# If php is set, prepend it
if php is not None:
cmd = php + ' ' + cmd
If composer has not been installed globally making it available in the
system PATH & making it executable, the ``composer`` and ``php`` parameters
will need to be set to the location of the executables.
# Add Working Dir
cmd += ' --working-dir=' + dir
dir
Directory location of the composer.json file.
# Other Settings
if quiet is True:
cmd += ' --quiet'
composer
Location of the composer.phar file. If not set composer will
just execute "composer" as if it is installed globally.
(i.e. /path/to/composer.phar)
if no_dev is True:
cmd += ' --no-dev'
php
Location of the php executable to use with composer.
(i.e. /usr/bin/php)
if prefer_source is True:
cmd += ' --prefer-source'
runas
Which system user to run composer as.
if prefer_dist is True:
cmd += ' --prefer-dist'
prefer_source
--prefer-source option of composer.
if no_scripts is True:
cmd += ' --no-scripts'
prefer_dist
--prefer-dist option of composer.
if no_plugins is True:
cmd += ' --no-plugins'
no_scripts
--no-scripts option of composer.
if optimize is True:
cmd += ' --optimize-autoloader'
no_plugins
--no-plugins option of composer.
result = __salt__['cmd.run_all'](cmd,
runas=runas,
env={'COMPOSER_HOME': composer_home},
python_shell=False)
optimize
--optimize-autoloader option of composer. Recommended for production.
if result['retcode'] != 0:
raise CommandExecutionError(result['stderr'])
no_dev
--no-dev option for composer. Recommended for production.
if quiet is True:
return True
quiet
--quiet option for composer. Whether or not to return output from composer.
return result['stdout']
composer_home
$COMPOSER_HOME environment variable
CLI Example:
.. code-block:: bash
salt '*' composer.update /var/www/application
salt '*' composer.update /var/www/application \
no_dev=True optimize=True
'''
result = _run_composer('update',
dir=dir,
composer=composer,
php=php,
runas=runas,
prefer_source=prefer_source,
prefer_dist=prefer_dist,
no_scripts=no_scripts,
no_plugins=no_plugins,
optimize=optimize,
no_dev=no_dev,
quiet=quiet,
composer_home=composer_home)
return result
def selfupdate(composer=None,
php=None,
runas=None,
quiet=False,
composer_home='/root'):
'''
Update composer itself.
If composer has not been installed globally making it available in the
system PATH & making it executable, the ``composer`` and ``php`` parameters
will need to be set to the location of the executables.
composer
Location of the composer.phar file. If not set composer will
just execute "composer" as if it is installed globally.
(i.e. /path/to/composer.phar)
php
Location of the php executable to use with composer.
(i.e. /usr/bin/php)
runas
Which system user to run composer as.
quiet
--quiet option for composer. Whether or not to return output from composer.
composer_home
$COMPOSER_HOME environment variable
CLI Example:
.. code-block:: bash
salt '*' composer.selfupdate
'''
result = _run_composer('selfupdate',
composer=composer,
php=php,
runas=runas,
quiet=quiet,
composer_home=composer_home)
return result

View file

@ -37,10 +37,10 @@ the location of composer in the state.
- php: /usr/local/bin/php
- no_dev: true
'''
from __future__ import absolute_import
# Import salt libs
import salt.utils
from salt.exceptions import CommandExecutionError, CommandNotFoundError
from salt.exceptions import SaltException
def __virtual__():
@ -53,7 +53,6 @@ def __virtual__():
def installed(name,
composer=None,
php=None,
runas=None,
user=None,
prefer_source=None,
prefer_dist=None,
@ -61,6 +60,7 @@ def installed(name,
no_plugins=None,
optimize=None,
no_dev=None,
quiet=False,
composer_home='/root'):
'''
Verify that composer has installed the latest packages give a
@ -78,11 +78,6 @@ def installed(name,
Location of the php executable to use with composer.
(i.e. /usr/bin/php)
runas
Which system user to run composer as.
.. deprecated:: 2014.1.4
user
Which system user to run composer as.
@ -114,29 +109,23 @@ def installed(name,
'''
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
salt.utils.warn_until(
'Lithium',
'Please remove \'runas\' support at this stage. \'user\' support was '
'added in 2014.1.4.',
_dont_call_warnings=True
)
if runas:
# Warn users about the deprecation
ret.setdefault('warnings', []).append(
'The \'runas\' argument is being deprecated in favor of \'user\', '
'please update your state files.'
)
if user is not None and runas is not None:
# user wins over runas but let warn about the deprecation.
ret.setdefault('warnings', []).append(
'Passed both the \'runas\' and \'user\' arguments. Please don\'t. '
'\'runas\' is being ignored in favor of \'user\'.'
)
runas = None
elif runas is not None:
# Support old runas usage
user = runas
runas = None
# Check if composer.lock exists, if so we already ran `composer install`
# and we don't need to do it again
if __salt__['composer.did_composer_install'](name):
ret['result'] = True
ret['comment'] = 'Composer already installed this directory'
return ret
# The state of the system does need to be changed. Check if we're running
# in ``test=true`` mode.
if __opts__['test'] is True:
ret['comment'] = 'The state of "{0}" will be changed.'.format(name)
ret['changes'] = {
'old': 'composer install has not yet been run in {0}'.format(name),
'new': 'composer install will be run in {0}'.format(name)
}
ret['result'] = None
return ret
try:
call = __salt__['composer.install'](
@ -150,22 +139,140 @@ def installed(name,
no_plugins=no_plugins,
optimize=optimize,
no_dev=no_dev,
quiet=False,
quiet=quiet,
composer_home=composer_home
)
except (CommandNotFoundError, CommandExecutionError) as err:
except (SaltException) as err:
ret['result'] = False
ret['comment'] = 'Error executing composer in \'{0!r}\': {1!r}'.format(name, err)
return ret
if call or isinstance(call, list) or isinstance(call, dict):
ret['result'] = True
if call.find('Nothing to install or update') < 0:
ret['changes']['stdout'] = call
# If composer retcode != 0 then an exception was thrown and we dealt with it.
# Any other case is success, regardless of what composer decides to output.
ret['comment'] = 'Composer ran, nothing changed in {0!r}'.format(name)
ret['result'] = True
if quiet is True:
ret['comment'] = 'Composer install completed successfully, output silenced by quiet flag'
else:
ret['result'] = False
ret['comment'] = 'Could not run composer'
ret['comment'] = 'Composer install completed successfully'
ret['changes'] = {
'stderr': call['stderr'],
'stdout': call['stdout']
}
return ret
def update(name,
composer=None,
php=None,
user=None,
prefer_source=None,
prefer_dist=None,
no_scripts=None,
no_plugins=None,
optimize=None,
no_dev=None,
quiet=False,
composer_home='/root'):
'''
Composer update the directory to ensure we have the latest versions
of all project dependencies.
dir
Directory location of the composer.json file.
composer
Location of the composer.phar file. If not set composer will
just execute "composer" as if it is installed globally.
(i.e. /path/to/composer.phar)
php
Location of the php executable to use with composer.
(i.e. /usr/bin/php)
user
Which system user to run composer as.
.. versionadded:: 2014.1.4
prefer_source
--prefer-source option of composer.
prefer_dist
--prefer-dist option of composer.
no_scripts
--no-scripts option of composer.
no_plugins
--no-plugins option of composer.
optimize
--optimize-autoloader option of composer. Recommended for production.
no_dev
--no-dev option for composer. Recommended for production.
quiet
--quiet option for composer. Whether or not to return output from composer.
composer_home
$COMPOSER_HOME environment variable
'''
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
# Check if composer.lock exists, if so we already ran `composer install`
is_installed = __salt__['composer.did_composer_install'](name)
if is_installed:
old_status = "composer install has not yet been run in {0}".format(name)
else:
old_status = "composer install has been run in {0}".format(name)
# The state of the system does need to be changed. Check if we're running
# in ``test=true`` mode.
if __opts__['test'] is True:
ret['comment'] = 'The state of "{0}" will be changed.'.format(name)
ret['changes'] = {
'old': old_status,
'new': 'composer install/update will be run in {0}'.format(name)
}
ret['result'] = None
return ret
try:
call = __salt__['composer.update'](
name,
composer=composer,
php=php,
runas=user,
prefer_source=prefer_source,
prefer_dist=prefer_dist,
no_scripts=no_scripts,
no_plugins=no_plugins,
optimize=optimize,
no_dev=no_dev,
quiet=quiet,
composer_home=composer_home
)
except (SaltException) as err:
ret['result'] = False
ret['comment'] = 'Error executing composer in \'{0!r}\': {1!r}'.format(name, err)
return ret
# If composer retcode != 0 then an exception was thrown and we dealt with it.
# Any other case is success, regardless of what composer decides to output.
ret['result'] = True
if quiet is True:
ret['comment'] = 'Composer update completed successfully, output silenced by quiet flag'
else:
ret['comment'] = 'Composer update completed successfully'
ret['changes'] = {
'stderr': call['stderr'],
'stdout': call['stdout']
}
return ret

View file

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Rupesh Tare <rupesht@saltstack.com>`
'''
# Import Python libs
from __future__ import absolute_import
# Import Salt Testing Libs
from salttesting import TestCase, skipIf
from salttesting.mock import (
MagicMock,
patch,
NO_MOCK,
NO_MOCK_REASON
)
# Import Salt Libs
from salt.modules import composer
from salt.exceptions import CommandExecutionError, CommandNotFoundError, SaltInvocationError
# Globals
composer.__grains__ = {}
composer.__salt__ = {}
composer.__context__ = {}
composer.__opts__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class ComposerTestCase(TestCase):
'''
Test cases for salt.modules.composer
'''
def test_install(self):
'''
Test for Install composer dependencies for a directory.
'''
mock = MagicMock(return_value=False)
with patch.object(composer, '_valid_composer', mock):
self.assertRaises(CommandNotFoundError, composer.install, 'd')
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
self.assertRaises(SaltInvocationError, composer.install, None)
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value={'retcode': 1, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertRaises(CommandExecutionError, composer.install, 'd')
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertTrue(composer.install('dir', None, None, None, None,
None, None, None, None, None,
True))
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
rval = {'retcode': 0, 'stderr': 'A', 'stdout': 'B'}
mock = MagicMock(return_value=rval)
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertEqual(composer.install('dir'), rval)
def test_update(self):
'''
Test for Update composer dependencies for a directory.
'''
mock = MagicMock(return_value=False)
with patch.object(composer, '_valid_composer', mock):
self.assertRaises(CommandNotFoundError, composer.update, 'd')
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=True)
with patch.object(composer, 'did_composer_install', mock):
self.assertRaises(SaltInvocationError, composer.update, None)
# Test update with error exit status throws exception
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=True)
with patch.object(composer, 'did_composer_install', mock):
mock = MagicMock(return_value={'retcode': 1, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertRaises(CommandExecutionError, composer.update, 'd')
# Test update with existing composer.lock and quiet=True
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=True)
with patch.object(composer, 'did_composer_install', mock):
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertTrue(composer.update('dir', None, None, None, None,
None, None, None, None, None,
True))
# Test update with no composer.lock and quiet=True
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=False)
with patch.object(composer, 'did_composer_install', mock):
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertTrue(composer.update('dir', None, None, None, None,
None, None, None, None, None,
True))
# Test update with existing composer.lock
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=True)
with patch.object(composer, 'did_composer_install', mock):
rval = {'retcode': 0, 'stderr': 'A', 'stdout': 'B'}
mock = MagicMock(return_value=rval)
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertEqual(composer.update('dir'), rval)
# Test update with no composer.lock
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value=False)
with patch.object(composer, 'did_composer_install', mock):
rval = {'retcode': 0, 'stderr': 'A', 'stdout': 'B'}
mock = MagicMock(return_value=rval)
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertEqual(composer.update('dir'), rval)
def test_selfupdate(self):
'''
Test for Install composer dependencies for a directory.
'''
mock = MagicMock(return_value=False)
with patch.object(composer, '_valid_composer', mock):
self.assertRaises(CommandNotFoundError, composer.selfupdate)
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value={'retcode': 1, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertRaises(CommandExecutionError, composer.selfupdate)
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'A'})
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertTrue(composer.selfupdate(quiet=True))
mock = MagicMock(return_value=True)
with patch.object(composer, '_valid_composer', mock):
rval = {'retcode': 0, 'stderr': 'A', 'stdout': 'B'}
mock = MagicMock(return_value=rval)
with patch.dict(composer.__salt__, {'cmd.run_all': mock}):
self.assertEqual(composer.selfupdate(), rval)
if __name__ == '__main__':
from integration import run_tests
run_tests(ComposerTestCase, needs_daemon=False)