Merge pull request #45774 from twangboy/mac_add_service_util

Fix __virtual__ issue in mac_system.py
This commit is contained in:
Nicole Thomas 2018-04-11 10:26:12 -04:00 committed by GitHub
commit 12ecfdee93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 518 additions and 105 deletions

View file

@ -29,14 +29,11 @@ def __virtual__():
'''
Only work on Mac OS
'''
if salt.utils.platform.is_darwin() \
and _LooseVersion(__grains__['osrelease']) >= _LooseVersion('10.9'):
return True
return (
False,
'The assistive module cannot be loaded: must be run on '
'macOS 10.9 or newer.'
)
if not salt.utils.platform.is_darwin():
return False, 'Must be run on macOS'
if not _LooseVersion(__grains__['osrelease']) >= salt.utils.stringutils.to_str('10.9'):
return False, 'Must be run on macOS 10.9 or newer'
return __virtualname__
def install(app_id, enable=True):

View file

@ -8,14 +8,13 @@ from __future__ import absolute_import, unicode_literals, print_function
# Import python libs
import os
import re
import plistlib
# Import salt libs
import salt.utils.decorators as decorators
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.mac_utils
from salt.exceptions import CommandExecutionError
from salt.utils.versions import LooseVersion as _LooseVersion
@ -53,73 +52,6 @@ def __virtual__():
return __virtualname__
def _launchd_paths():
'''
Paths where launchd services can be found
'''
return [
'/Library/LaunchAgents',
'/Library/LaunchDaemons',
'/System/Library/LaunchAgents',
'/System/Library/LaunchDaemons',
]
@decorators.memoize
def _available_services():
'''
Return a dictionary of all available services on the system
'''
available_services = dict()
for launch_dir in _launchd_paths():
for root, dirs, files in salt.utils.path.os_walk(launch_dir):
for file_name in files:
# Must be a plist file
if not file_name.endswith('.plist'):
continue
# Follow symbolic links of files in _launchd_paths
file_path = os.path.join(root, file_name)
true_path = os.path.realpath(file_path)
# ignore broken symlinks
if not os.path.exists(true_path):
continue
try:
# This assumes most of the plist files
# will be already in XML format
with salt.utils.files.fopen(file_path):
plist = plistlib.readPlist(true_path)
except Exception:
# If plistlib is unable to read the file we'll need to use
# the system provided plutil program to do the conversion
cmd = '/usr/bin/plutil -convert xml1 -o - -- "{0}"'.format(
true_path)
plist_xml = __salt__['cmd.run'](cmd, output_loglevel='quiet')
if six.PY2:
plist = plistlib.readPlistFromString(plist_xml)
else:
plist = plistlib.readPlistFromBytes(
salt.utils.stringutils.to_bytes(plist_xml))
try:
available_services[plist.Label.lower()] = {
'file_name': file_name,
'file_path': true_path,
'plist': plist}
except AttributeError:
# Handle malformed plist files
available_services[os.path.basename(file_name).lower()] = {
'file_name': file_name,
'file_path': true_path,
'plist': plist}
return available_services
def _get_service(name):
'''
Get information about a service. If the service is not found, raise an
@ -130,7 +62,7 @@ def _get_service(name):
:return: The service information for the service, otherwise an Error
:rtype: dict
'''
services = _available_services()
services = salt.utils.mac_utils.available_services()
name = name.lower()
if name in services:
@ -195,26 +127,7 @@ def launchctl(sub_cmd, *args, **kwargs):
salt '*' service.launchctl debug org.cups.cupsd
'''
# Get return type
return_stdout = kwargs.pop('return_stdout', False)
# Construct command
cmd = ['launchctl', sub_cmd]
cmd.extend(args)
# Run command
kwargs['python_shell'] = False
ret = __salt__['cmd.run_all'](cmd, **kwargs)
# Raise an error or return successful result
if ret['retcode']:
out = 'Failed to {0} service:\n'.format(sub_cmd)
out += 'stdout: {0}\n'.format(ret['stdout'])
out += 'stderr: {0}\n'.format(ret['stderr'])
out += 'retcode: {0}\n'.format(ret['retcode'])
raise CommandExecutionError(out)
else:
return ret['stdout'] if return_stdout else True
return salt.utils.mac_utils.launchctl(sub_cmd, *args, **kwargs)
def list_(name=None, runas=None):
@ -541,7 +454,7 @@ def get_all(runas=None):
enabled = get_enabled(runas=runas)
# Get list of all services
available = list(_available_services().keys())
available = list(salt.utils.mac_utils.available_services().keys())
# Return composite list
return sorted(set(enabled + available))

View file

@ -8,6 +8,7 @@ System module for sleeping, restarting, and shutting down the system on Mac OS X
Using this module will enable ``atrun`` on the system if it is disabled.
'''
from __future__ import absolute_import, unicode_literals, print_function
import os
# Import python libs
try: # python 3
@ -18,9 +19,10 @@ except ImportError: # python 2
import getpass
# Import salt libs
from salt.ext import six
import salt.utils.mac_utils
import salt.utils.platform
from salt.exceptions import SaltInvocationError
from salt.exceptions import SaltInvocationError, CommandExecutionError
__virtualname__ = 'system'
@ -47,15 +49,71 @@ def _atrun_enabled():
'''
Check to see if atrun is enabled on the system
'''
return __salt__['service.enabled']('com.apple.atrun')
name = 'com.apple.atrun'
services = salt.utils.mac_utils.available_services()
label = None
if name in services:
label = services[name]['plist']['Label']
else:
for service in six.itervalues(services):
if service['file_path'].lower() == name:
# Match on full path
label = service['plist']['Label']
break
basename, ext = os.path.splitext(service['file_name'])
if basename.lower() == name:
# Match on basename
label = service['plist']['Label']
break
if not label:
return False
try:
# Collect information on service: will raise an error if it fails
salt.utils.mac_utils.launchctl('list',
label,
return_stdout=True,
output_loglevel='quiet')
return True
except CommandExecutionError:
return False
def _enable_atrun():
'''
Enable and start the atrun daemon
'''
__salt__['service.enable']('com.apple.atrun')
__salt__['service.start']('com.apple.atrun')
name = 'com.apple.atrun'
services = salt.utils.mac_utils.available_services()
label = None
path = None
if name in services:
label = services[name]['plist']['Label']
path = services[name]['file_path']
else:
for service in six.itervalues(services):
if service['file_path'].lower() == name:
# Match on full path
label = service['plist']['Label']
path = service['file_path']
break
basename, ext = os.path.splitext(service['file_name'])
if basename.lower() == name:
# Match on basename
label = service['plist']['Label']
path = service['file_path']
break
if not label:
return False
salt.utils.mac_utils.launchctl('enable',
'system/{0}'.format(label),
output_loglevel='quiet')
salt.utils.mac_utils.launchctl('load', path, output_loglevel='quiet')
return _atrun_enabled()

View file

@ -9,15 +9,19 @@ from __future__ import absolute_import, unicode_literals
import logging
import subprocess
import os
import plistlib
import time
# Import Salt Libs
import salt.modules.cmdmod
import salt.utils.args
import salt.utils.decorators as decorators
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.timed_subprocess
import salt.grains.extra
from salt.ext import six
from salt.exceptions import CommandExecutionError, SaltInvocationError,\
TimedProcTimeoutError
@ -227,3 +231,131 @@ def confirm_updated(value, check_fun, normalize_ret=False, wait=5):
return True
time.sleep(1)
return False
def launchctl(sub_cmd, *args, **kwargs):
'''
Run a launchctl command and raise an error if it fails
Args: additional args are passed to launchctl
sub_cmd (str): Sub command supplied to launchctl
Kwargs: passed to ``cmd.run_all``
return_stdout (bool): A keyword argument. If true return the stdout of
the launchctl command
Returns:
bool: ``True`` if successful
str: The stdout of the launchctl command if requested
Raises:
CommandExecutionError: If command fails
CLI Example:
.. code-block:: bash
import salt.utils.mac_service
salt.utils.mac_service.launchctl('debug', 'org.cups.cupsd')
'''
# Get return type
return_stdout = kwargs.pop('return_stdout', False)
# Construct command
cmd = ['launchctl', sub_cmd]
cmd.extend(args)
# Run command
kwargs['python_shell'] = False
ret = salt.modules.cmdmod.run_all(cmd, **kwargs)
# Raise an error or return successful result
if ret['retcode']:
out = 'Failed to {0} service:\n'.format(sub_cmd)
out += 'stdout: {0}\n'.format(ret['stdout'])
out += 'stderr: {0}\n'.format(ret['stderr'])
out += 'retcode: {0}'.format(ret['retcode'])
raise CommandExecutionError(out)
else:
return ret['stdout'] if return_stdout else True
def _available_services():
'''
This is a helper function needed for testing. We are using the memoziation
decorator on the `available_services` function, which causes the function
to run once and then return the results of the first run on subsequent
calls. This causes problems when trying to test the functionality of the
`available_services` function.
'''
launchd_paths = [
'/Library/LaunchAgents',
'/Library/LaunchDaemons',
'/System/Library/LaunchAgents',
'/System/Library/LaunchDaemons',
]
_available_services = dict()
for launch_dir in launchd_paths:
for root, dirs, files in salt.utils.path.os_walk(launch_dir):
for file_name in files:
# Must be a plist file
if not file_name.endswith('.plist'):
continue
# Follow symbolic links of files in _launchd_paths
file_path = os.path.join(root, file_name)
true_path = os.path.realpath(file_path)
# ignore broken symlinks
if not os.path.exists(true_path):
continue
try:
# This assumes most of the plist files
# will be already in XML format
plist = plistlib.readPlist(true_path)
except Exception:
# If plistlib is unable to read the file we'll need to use
# the system provided plutil program to do the conversion
cmd = '/usr/bin/plutil -convert xml1 -o - -- "{0}"'.format(
true_path)
plist_xml = salt.modules.cmdmod.run(cmd, output_loglevel='quiet')
if six.PY2:
plist = plistlib.readPlistFromString(plist_xml)
else:
plist = plistlib.loads(
salt.utils.stringutils.to_bytes(plist_xml))
try:
_available_services[plist.Label.lower()] = {
'file_name': file_name,
'file_path': true_path,
'plist': plist}
except AttributeError:
# Handle malformed plist files
_available_services[os.path.basename(file_name).lower()] = {
'file_name': file_name,
'file_path': true_path,
'plist': plist}
return _available_services
@decorators.memoize
def available_services():
'''
Return a dictionary of all available services on the system
Returns:
dict: All available services
CLI Example:
.. code-block:: bash
import salt.utils.mac_service
salt.utils.mac_service.available_services()
'''
return _available_services()

View file

@ -5,10 +5,11 @@ mac_utils tests
# Import python libs
from __future__ import absolute_import, unicode_literals
import os
# Import Salt Testing Libs
from tests.support.unit import TestCase, skipIf
from tests.support.mock import MagicMock, patch, NO_MOCK, NO_MOCK_REASON
from tests.support.mock import MagicMock, patch, NO_MOCK, NO_MOCK_REASON, call
# Import Salt libs
import salt.utils.mac_utils as mac_utils
@ -16,6 +17,7 @@ from salt.exceptions import SaltInvocationError, CommandExecutionError
# Import 3rd-party libs
from salt.ext.six.moves import range
from salt.ext import six
@skipIf(NO_MOCK, NO_MOCK_REASON)
@ -165,3 +167,314 @@ class MacUtilsTestCase(TestCase):
'''
self.assertEqual(mac_utils.validate_enabled(False),
'off')
def test_launchctl(self):
'''
test launchctl function
'''
mock_cmd = MagicMock(return_value={'retcode': 0,
'stdout': 'success',
'stderr': 'none'})
with patch('salt.modules.cmdmod.run_all', mock_cmd) as m_run_all:
ret = mac_utils.launchctl('enable', 'org.salt.minion')
m_run_all.assert_called_with(
['launchctl', 'enable', 'org.salt.minion'],
python_shell=False)
self.assertEqual(ret, True)
def test_launchctl_return_stdout(self):
'''
test launchctl function and return stdout
'''
mock_cmd = MagicMock(return_value={'retcode': 0,
'stdout': 'success',
'stderr': 'none'})
with patch('salt.modules.cmdmod.run_all', mock_cmd) as m_run_all:
ret = mac_utils.launchctl('enable',
'org.salt.minion',
return_stdout=True)
m_run_all.assert_called_with(['launchctl', 'enable', 'org.salt.minion'],
python_shell=False)
self.assertEqual(ret, 'success')
def test_launchctl_error(self):
'''
test launchctl function returning an error
'''
mock_cmd = MagicMock(return_value={'retcode': 1,
'stdout': 'failure',
'stderr': 'test failure'})
error = 'Failed to enable service:\n' \
'stdout: failure\n' \
'stderr: test failure\n' \
'retcode: 1'
with patch('salt.modules.cmdmod.run_all', mock_cmd) as m_run_all:
try:
mac_utils.launchctl('enable', 'org.salt.minion')
except CommandExecutionError as exc:
self.assertEqual(exc.message, error)
m_run_all.assert_called_with(['launchctl', 'enable', 'org.salt.minion'],
python_shell=False)
@patch('salt.utils.path.os_walk')
@patch('os.path.exists')
@patch('plistlib.readPlist')
def test_available_services(self, mock_read_plist, mock_exists, mock_os_walk):
'''
test available_services
'''
mock_os_walk.side_effect = [
[('/Library/LaunchAgents', [], ['com.apple.lla1.plist', 'com.apple.lla2.plist'])],
[('/Library/LaunchDaemons', [], ['com.apple.lld1.plist', 'com.apple.lld2.plist'])],
[('/System/Library/LaunchAgents', [], ['com.apple.slla1.plist', 'com.apple.slla2.plist'])],
[('/System/Library/LaunchDaemons', [], ['com.apple.slld1.plist', 'com.apple.slld2.plist'])],
]
mock_read_plist.side_effect = [
MagicMock(Label='com.apple.lla1'),
MagicMock(Label='com.apple.lla2'),
MagicMock(Label='com.apple.lld1'),
MagicMock(Label='com.apple.lld2'),
MagicMock(Label='com.apple.slla1'),
MagicMock(Label='com.apple.slla2'),
MagicMock(Label='com.apple.slld1'),
MagicMock(Label='com.apple.slld2'),
]
mock_exists.return_value = True
ret = mac_utils._available_services()
# Make sure it's a dict with 8 items
self.assertTrue(isinstance(ret, dict))
self.assertEqual(len(ret), 8)
self.assertEqual(
ret['com.apple.lla1']['file_name'],
'com.apple.lla1.plist')
self.assertEqual(
ret['com.apple.lla1']['file_path'],
os.path.realpath(
os.path.join('/Library/LaunchAgents', 'com.apple.lla1.plist')))
self.assertEqual(
ret['com.apple.slld2']['file_name'],
'com.apple.slld2.plist')
self.assertEqual(
ret['com.apple.slld2']['file_path'],
os.path.realpath(
os.path.join('/System/Library/LaunchDaemons', 'com.apple.slld2.plist')))
@patch('salt.utils.path.os_walk')
@patch('os.path.exists')
@patch('plistlib.readPlist')
def test_available_services_broken_symlink(self, mock_read_plist, mock_exists, mock_os_walk):
'''
test available_services
'''
mock_os_walk.side_effect = [
[('/Library/LaunchAgents', [], ['com.apple.lla1.plist', 'com.apple.lla2.plist'])],
[('/Library/LaunchDaemons', [], ['com.apple.lld1.plist', 'com.apple.lld2.plist'])],
[('/System/Library/LaunchAgents', [], ['com.apple.slla1.plist', 'com.apple.slla2.plist'])],
[('/System/Library/LaunchDaemons', [], ['com.apple.slld1.plist', 'com.apple.slld2.plist'])],
]
mock_read_plist.side_effect = [
MagicMock(Label='com.apple.lla1'),
MagicMock(Label='com.apple.lla2'),
MagicMock(Label='com.apple.lld1'),
MagicMock(Label='com.apple.lld2'),
MagicMock(Label='com.apple.slld1'),
MagicMock(Label='com.apple.slld2'),
]
mock_exists.side_effect = [True, True, True, True, False, False, True, True]
ret = mac_utils._available_services()
# Make sure it's a dict with 6 items
self.assertTrue(isinstance(ret, dict))
self.assertEqual(len(ret), 6)
self.assertEqual(
ret['com.apple.lla1']['file_name'],
'com.apple.lla1.plist')
self.assertEqual(
ret['com.apple.lla1']['file_path'],
os.path.realpath(
os.path.join('/Library/LaunchAgents', 'com.apple.lla1.plist')))
self.assertEqual(
ret['com.apple.slld2']['file_name'],
'com.apple.slld2.plist')
self.assertEqual(
ret['com.apple.slld2']['file_path'],
os.path.realpath(
os.path.join('/System/Library/LaunchDaemons', 'com.apple.slld2.plist')))
@patch('salt.utils.path.os_walk')
@patch('os.path.exists')
@patch('plistlib.readPlist')
@patch('salt.modules.cmdmod.run')
@patch('plistlib.readPlistFromString' if six.PY2 else 'plistlib.loads')
def test_available_services_non_xml(self,
mock_read_plist_from_string,
mock_run,
mock_read_plist,
mock_exists,
mock_os_walk):
'''
test available_services
'''
mock_os_walk.side_effect = [
[('/Library/LaunchAgents', [], ['com.apple.lla1.plist', 'com.apple.lla2.plist'])],
[('/Library/LaunchDaemons', [], ['com.apple.lld1.plist', 'com.apple.lld2.plist'])],
[('/System/Library/LaunchAgents', [], ['com.apple.slla1.plist', 'com.apple.slla2.plist'])],
[('/System/Library/LaunchDaemons', [], ['com.apple.slld1.plist', 'com.apple.slld2.plist'])],
]
mock_exists.return_value = True
mock_read_plist.side_effect = Exception()
mock_run.return_value = '<some xml>'
mock_read_plist_from_string.side_effect = [
MagicMock(Label='com.apple.lla1'),
MagicMock(Label='com.apple.lla2'),
MagicMock(Label='com.apple.lld1'),
MagicMock(Label='com.apple.lld2'),
MagicMock(Label='com.apple.slla1'),
MagicMock(Label='com.apple.slla2'),
MagicMock(Label='com.apple.slld1'),
MagicMock(Label='com.apple.slld2'),
]
ret = mac_utils._available_services()
cmd = '/usr/bin/plutil -convert xml1 -o - -- "{0}"'
calls = [
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchAgents', 'com.apple.lla1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchAgents', 'com.apple.lla2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchDaemons', 'com.apple.lld1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchDaemons', 'com.apple.lld2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchAgents', 'com.apple.slla1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchAgents', 'com.apple.slla2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchDaemons', 'com.apple.slld1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchDaemons', 'com.apple.slld2.plist'))),
output_loglevel='quiet'),
]
mock_run.assert_has_calls(calls)
# Make sure it's a dict with 8 items
self.assertTrue(isinstance(ret, dict))
self.assertEqual(len(ret), 8)
self.assertEqual(
ret['com.apple.lla1']['file_name'],
'com.apple.lla1.plist')
self.assertEqual(
ret['com.apple.lla1']['file_path'],
os.path.realpath(
os.path.join('/Library/LaunchAgents', 'com.apple.lla1.plist')))
self.assertEqual(
ret['com.apple.slld2']['file_name'],
'com.apple.slld2.plist')
self.assertEqual(
ret['com.apple.slld2']['file_path'],
os.path.realpath(
os.path.join('/System/Library/LaunchDaemons', 'com.apple.slld2.plist')))
@patch('salt.utils.path.os_walk')
@patch('os.path.exists')
@patch('plistlib.readPlist')
@patch('salt.modules.cmdmod.run')
@patch('plistlib.readPlistFromString' if six.PY2 else 'plistlib.loads')
def test_available_services_non_xml_malformed_plist(self,
mock_read_plist_from_string,
mock_run,
mock_read_plist,
mock_exists,
mock_os_walk):
'''
test available_services
'''
mock_os_walk.side_effect = [
[('/Library/LaunchAgents', [], ['com.apple.lla1.plist', 'com.apple.lla2.plist'])],
[('/Library/LaunchDaemons', [], ['com.apple.lld1.plist', 'com.apple.lld2.plist'])],
[('/System/Library/LaunchAgents', [], ['com.apple.slla1.plist', 'com.apple.slla2.plist'])],
[('/System/Library/LaunchDaemons', [], ['com.apple.slld1.plist', 'com.apple.slld2.plist'])],
]
mock_exists.return_value = True
mock_read_plist.side_effect = Exception()
mock_run.return_value = '<some xml>'
mock_read_plist_from_string.return_value = 'malformedness'
ret = mac_utils._available_services()
cmd = '/usr/bin/plutil -convert xml1 -o - -- "{0}"'
calls = [
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchAgents', 'com.apple.lla1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchAgents', 'com.apple.lla2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchDaemons', 'com.apple.lld1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/Library/LaunchDaemons', 'com.apple.lld2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchAgents', 'com.apple.slla1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchAgents', 'com.apple.slla2.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchDaemons', 'com.apple.slld1.plist'))),
output_loglevel='quiet'),
call(cmd.format(os.path.realpath(os.path.join(
'/System/Library/LaunchDaemons', 'com.apple.slld2.plist'))),
output_loglevel='quiet'),
]
mock_run.assert_has_calls(calls)
# Make sure it's a dict with 8 items
self.assertTrue(isinstance(ret, dict))
self.assertEqual(len(ret), 8)
self.assertEqual(
ret['com.apple.lla1.plist']['file_name'],
'com.apple.lla1.plist')
self.assertEqual(
ret['com.apple.lla1.plist']['file_path'],
os.path.realpath(
os.path.join('/Library/LaunchAgents', 'com.apple.lla1.plist')))
self.assertEqual(
ret['com.apple.slld2.plist']['file_name'],
'com.apple.slld2.plist')
self.assertEqual(
ret['com.apple.slld2.plist']['file_path'],
os.path.realpath(
os.path.join('/System/Library/LaunchDaemons', 'com.apple.slld2.plist')))