File: //proc/self/root/usr/lib/python2.7/site-packages/salt/modules/mac_service.py
# -*- coding: utf-8 -*-
'''
The service module for macOS
.. versionadded:: 2016.3.0
This module has support for services in the following locations.
.. code-block:: bash
/System/Library/LaunchDaemons/
/System/Library/LaunchAgents/
/Library/LaunchDaemons/
/Library/LaunchAgents/
# As of version "2019.2.0" support for user-specific services were added.
/Users/foo/Library/LaunchAgents/
.. note::
As of the 2019.2.0 release, if a service is located in a ``LaunchAgent``
path and a ``runas`` user is NOT specified, the current console user will
be used to properly interact with the service.
'''
from __future__ import absolute_import, unicode_literals, print_function
# Import python libs
import logging
import os
import re
# Import salt libs
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
from salt.exceptions import CommandExecutionError
from salt.utils.versions import LooseVersion as _LooseVersion
# Import 3rd party libs
from salt.ext import six
# Define the module's virtual name
__virtualname__ = 'service'
__func_alias__ = {
'list_': 'list',
}
log = logging.getLogger(__name__)
def __virtual__():
'''
Only for macOS with launchctl
'''
if not salt.utils.platform.is_darwin():
return (False, 'Failed to load the mac_service module:\n'
'Only available on macOS systems.')
if not salt.utils.path.which('launchctl'):
return (False, 'Failed to load the mac_service module:\n'
'Required binary not found: "launchctl"')
if not salt.utils.path.which('plutil'):
return (False, 'Failed to load the mac_service module:\n'
'Required binary not found: "plutil"')
if _LooseVersion(__grains__['osrelease']) < _LooseVersion('10.11'):
return (False, 'Failed to load the mac_service module:\n'
'Requires macOS 10.11 or newer')
return __virtualname__
def _name_in_services(name, services):
'''
Checks to see if the given service is in the given services.
:param str name: Service label, file name, or full path
:param dict services: The currently available services.
:return: The service information for the service, otherwise
an empty dictionary
:rtype: dict
'''
if name in services:
# Match on label
return services[name]
for service in six.itervalues(services):
if service['file_path'].lower() == name:
# Match on full path
return service
basename, ext = os.path.splitext(service['file_name'])
if basename.lower() == name:
# Match on basename
return service
return dict()
def _get_service(name):
'''
Get information about a service. If the service is not found, raise an
error
:param str name: Service label, file name, or full path
:return: The service information for the service, otherwise an Error
:rtype: dict
'''
services = __utils__['mac_utils.available_services']()
name = name.lower()
service = _name_in_services(name, services)
# if we would the service we can return it
if service:
return service
# if we got here our service is not available, now we can check to see if
# we received a cached batch of services, if not we did a fresh check
# so we need to raise that the service could not be found.
try:
if not __context__['using_cached_services']:
raise CommandExecutionError('Service not found: {0}'.format(name))
except KeyError:
pass
# we used a cached version to check, a service could have been made
# between now and then, we should refresh our available services.
services = __utils__['mac_utils.available_services'](refresh=True)
# check to see if we found the service we are looking for.
service = _name_in_services(name, services)
if not service:
# Could not find the service after refresh raise.
raise CommandExecutionError('Service not found: {0}'.format(name))
# found it :)
return service
def _always_running_service(name):
'''
Check if the service should always be running based on the KeepAlive Key
in the service plist.
:param str name: Service label, file name, or full path
:return: True if the KeepAlive key is set to True, False if set to False or
not set in the plist at all.
:rtype: bool
.. versionadded:: 2019.2.0
'''
# get all the info from the launchctl service
service_info = show(name)
# get the value for the KeepAlive key in service plist
try:
keep_alive = service_info['plist']['KeepAlive']
except KeyError:
return False
# check if KeepAlive is True and not just set.
if isinstance(keep_alive, dict):
# check for pathstate
for _file, value in six.iteritems(keep_alive.get('PathState', {})):
if value is True and os.path.exists(_file):
return True
elif value is False and not os.path.exists(_file):
return True
if keep_alive is True:
return True
return False
def _get_domain_target(name, service_target=False):
'''
Returns the domain/service target and path for a service. This is used to
determine whether or not a service should be loaded in a user space or
system space.
:param str name: Service label, file name, or full path
:param bool service_target: Whether to return a full
service target. This is needed for the enable and disable
subcommands of /bin/launchctl. Defaults to False
:return: Tuple of the domain/service target and the path to the service.
:rtype: tuple
.. versionadded:: 2019.2.0
'''
# Get service information
service = _get_service(name)
# get the path to the service
path = service['file_path']
# most of the time we'll be at the system level.
domain_target = 'system'
# check if a LaunchAgent as we should treat these differently.
if 'LaunchAgents' in path:
# Get the console user so we can service in the correct session
uid = __utils__['mac_utils.console_user']()
domain_target = 'gui/{}'.format(uid)
# check to see if we need to make it a full service target.
if service_target is True:
domain_target = '{}/{}'.format(domain_target, service['plist']['Label'])
return (domain_target, path)
def _launch_agent(name):
'''
Checks to see if the provided service is a LaunchAgent
:param str name: Service label, file name, or full path
:return: True if a LaunchAgent, False if not.
:rtype: bool
.. versionadded:: 2019.2.0
'''
# Get the path to the service.
path = _get_service(name)['file_path']
if 'LaunchAgents' not in path:
return False
return True
def show(name):
'''
Show properties of a launchctl service
:param str name: Service label, file name, or full path
:return: The service information if the service is found
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' service.show org.cups.cupsd # service label
salt '*' service.show org.cups.cupsd.plist # file name
salt '*' service.show /System/Library/LaunchDaemons/org.cups.cupsd.plist # full path
'''
return _get_service(name)
def launchctl(sub_cmd, *args, **kwargs):
'''
Run a launchctl command and raise an error if it fails
:param str sub_cmd: Sub command supplied to launchctl
:param tuple args: Tuple containing additional arguments to pass to
launchctl
:param dict kwargs: Dictionary containing arguments to pass to
``cmd.run_all``
:param bool return_stdout: A keyword argument. If true return the stdout
of the launchctl command
:return: ``True`` if successful, raise ``CommandExecutionError`` if not, or
the stdout of the launchctl command if requested
:rtype: bool, str
CLI Example:
.. code-block:: bash
salt '*' service.launchctl debug org.cups.cupsd
'''
return __utils__['mac_utils.launchctl'](sub_cmd, *args, **kwargs)
def list_(name=None, runas=None):
'''
Run launchctl list and return the output
:param str name: The name of the service to list
:param str runas: User to run launchctl commands
:return: If a name is passed returns information about the named service,
otherwise returns a list of all services and pids
:rtype: str
CLI Example:
.. code-block:: bash
salt '*' service.list
salt '*' service.list org.cups.cupsd
'''
if name:
# Get service information and label
service = _get_service(name)
label = service['plist']['Label']
# we can assume if we are trying to list a LaunchAgent we need
# to run as a user, if not provided, we'll use the console user.
if not runas and _launch_agent(name):
runas = __utils__['mac_utils.console_user'](username=True)
# Collect information on service: will raise an error if it fails
return launchctl('list',
label,
return_stdout=True,
runas=runas)
# Collect information on all services: will raise an error if it fails
return launchctl('list',
return_stdout=True,
runas=runas)
def enable(name, runas=None):
'''
Enable a launchd service. Raises an error if the service fails to be enabled
:param str name: Service label, file name, or full path
:param str runas: User to run launchctl commands
:return: ``True`` if successful or if the service is already enabled
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.enable org.cups.cupsd
'''
# Get the domain target. enable requires a full <service-target>
service_target = _get_domain_target(name, service_target=True)[0]
# Enable the service: will raise an error if it fails
return launchctl('enable', service_target, runas=runas)
def disable(name, runas=None):
'''
Disable a launchd service. Raises an error if the service fails to be
disabled
:param str name: Service label, file name, or full path
:param str runas: User to run launchctl commands
:return: ``True`` if successful or if the service is already disabled
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.disable org.cups.cupsd
'''
# Get the service target. enable requires a full <service-target>
service_target = _get_domain_target(name, service_target=True)[0]
# disable the service: will raise an error if it fails
return launchctl('disable', service_target, runas=runas)
def start(name, runas=None):
'''
Start a launchd service. Raises an error if the service fails to start
.. note::
To start a service in macOS the service must be enabled first. Use
``service.enable`` to enable the service.
:param str name: Service label, file name, or full path
:param str runas: User to run launchctl commands
:return: ``True`` if successful or if the service is already running
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.start org.cups.cupsd
'''
# Get the domain target.
domain_target, path = _get_domain_target(name)
# Load (bootstrap) the service: will raise an error if it fails
return launchctl('bootstrap', domain_target, path, runas=runas)
def stop(name, runas=None):
'''
Stop a launchd service. Raises an error if the service fails to stop
.. note::
Though ``service.stop`` will unload a service in macOS, the service
will start on next boot unless it is disabled. Use ``service.disable``
to disable the service
:param str name: Service label, file name, or full path
:param str runas: User to run launchctl commands
:return: ``True`` if successful or if the service is already stopped
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.stop org.cups.cupsd
'''
# Get the domain target.
domain_target, path = _get_domain_target(name)
# Stop (bootout) the service: will raise an error if it fails
return launchctl('bootout', domain_target, path, runas=runas)
def restart(name, runas=None):
'''
Unloads and reloads a launchd service. Raises an error if the service
fails to reload
:param str name: Service label, file name, or full path
:param str runas: User to run launchctl commands
:return: ``True`` if successful
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.restart org.cups.cupsd
'''
# Restart the service: will raise an error if it fails
if enabled(name):
stop(name, runas=runas)
start(name, runas=runas)
return True
def status(name, sig=None, runas=None):
'''
Return the status for a service.
:param str name: Used to find the service from launchctl. Can be any part
of the service name or a regex expression.
:param str sig: Find the service with status.pid instead. Note that
``name`` must still be provided.
:param str runas: User to run launchctl commands
:return: The PID for the service if it is running, or 'loaded' if the
service should not always have a PID, or otherwise an empty string
:rtype: str
CLI Example:
.. code-block:: bash
salt '*' service.status cups
'''
# Find service with ps
if sig:
return __salt__['status.pid'](sig)
try:
_get_service(name)
except CommandExecutionError as msg:
log.error(msg)
return ''
if not runas and _launch_agent(name):
runas = __utils__['mac_utils.console_user'](username=True)
output = list_(runas=runas)
# Used a string here instead of a list because that's what the linux version
# of this module does
pids = ''
for line in output.splitlines():
if 'PID' in line:
continue
if re.search(name, line.split()[-1]):
if line.split()[0].isdigit():
if pids:
pids += '\n'
pids += line.split()[0]
# mac services are a little different than other platforms as they may be
# set to run on intervals and may not always active with a PID. This will
# return a string 'loaded' if it shouldn't always be running and is enabled.
if not _always_running_service(name) and enabled(name) and not pids:
return 'loaded'
return pids
def available(name):
'''
Check that the given service is available.
:param str name: The name of the service
:return: True if the service is available, otherwise False
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.available com.openssh.sshd
'''
try:
_get_service(name)
return True
except CommandExecutionError:
return False
def missing(name):
'''
The inverse of service.available
Check that the given service is not available.
:param str name: The name of the service
:return: True if the service is not available, otherwise False
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.missing com.openssh.sshd
'''
return not available(name)
def enabled(name, runas=None):
'''
Check if the specified service is enabled
:param str name: The name of the service to look up
:param str runas: User to run launchctl commands
:return: True if the specified service enabled, otherwise False
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.enabled org.cups.cupsd
'''
# Try to list the service. If it can't be listed, it's not enabled
try:
list_(name=name, runas=runas)
return True
except CommandExecutionError:
return False
def disabled(name, runas=None, domain='system'):
'''
Check if the specified service is not enabled. This is the opposite of
``service.enabled``
:param str name: The name to look up
:param str runas: User to run launchctl commands
:param str domain: domain to check for disabled services. Default is system.
:return: True if the specified service is NOT enabled, otherwise False
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' service.disabled org.cups.cupsd
'''
disabled = launchctl('print-disabled',
domain,
return_stdout=True,
runas=runas)
for service in disabled.split("\n"):
if name in service:
srv_name = service.split("=>")[0].split("\"")[1]
status = service.split("=>")[1]
if name != srv_name:
pass
else:
return True if 'true' in status.lower() else False
return False
def get_all(runas=None):
'''
Return a list of services that are enabled or available. Can be used to
find the name of a service.
:param str runas: User to run launchctl commands
:return: A list of all the services available or enabled
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' service.get_all
'''
# Get list of enabled services
enabled = get_enabled(runas=runas)
# Get list of all services
available = list(__utils__['mac_utils.available_services']().keys())
# Return composite list
return sorted(set(enabled + available))
def get_enabled(runas=None):
'''
Return a list of all services that are enabled. Can be used to find the
name of a service.
:param str runas: User to run launchctl commands
:return: A list of all the services enabled on the system
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' service.get_enabled
'''
# Collect list of enabled services
stdout = list_(runas=runas)
service_lines = [line for line in stdout.splitlines()]
# Construct list of enabled services
enabled = []
for line in service_lines:
# Skip header line
if line.startswith('PID'):
continue
pid, status, label = line.split('\t')
enabled.append(label)
return sorted(set(enabled))