From 6fbfdf3bdcebf91c31e55bc3377502b05a5cac9b Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 5 Nov 2018 17:32:36 +0100 Subject: [PATCH] systemd: add optional root parameter With systemctl is possible for certain actions (enable, disable, mask, and others) indicate a different root directory. If this parameter is specified, systemctl will do the work independently of systemd service. This patch add the optional root parameter to those actions. (cherry picked from commit 786688f4d56bb567ed29f5764ff0c2d8e0995176) --- salt/modules/systemd_service.py | 209 +++++++++++++-------- tests/unit/modules/test_systemd_service.py | 4 +- 2 files changed, 137 insertions(+), 76 deletions(-) diff --git a/salt/modules/systemd_service.py b/salt/modules/systemd_service.py index fb349d30e6c7..3efd455ba689 100644 --- a/salt/modules/systemd_service.py +++ b/salt/modules/systemd_service.py @@ -56,7 +56,7 @@ def __virtual__(): Only work on systems that have been booted with systemd ''' if __grains__['kernel'] == 'Linux' \ - and salt.utils.systemd.booted(__context__): + and salt.utils.systemd.booted(__context__): return __virtualname__ return ( False, @@ -65,6 +65,16 @@ def __virtual__(): ) +def _root(path, root): + ''' + Relocate an absolute path to a new root directory. + ''' + if root: + return os.path.join(root, os.path.relpath(path, os.path.sep)) + else: + return path + + def _canonical_unit_name(name): ''' Build a canonical unit name treating unit names without one @@ -123,15 +133,15 @@ def _check_for_unit_changes(name): __context__[contextkey] = True -def _check_unmask(name, unmask, unmask_runtime): +def _check_unmask(name, unmask, unmask_runtime, root=None): ''' Common code for conditionally removing masks before making changes to a service's state. ''' if unmask: - unmask_(name, runtime=False) + unmask_(name, runtime=False, root=root) if unmask_runtime: - unmask_(name, runtime=True) + unmask_(name, runtime=True, root=root) def _clear_context(): @@ -193,15 +203,16 @@ def _default_runlevel(): return runlevel -def _get_systemd_services(): +def _get_systemd_services(root): ''' Use os.listdir() to get all the unit files ''' ret = set() for path in SYSTEM_CONFIG_PATHS + (LOCAL_CONFIG_PATH,): - # Make sure user has access to the path, and if the path is a link - # it's likely that another entry in SYSTEM_CONFIG_PATHS or LOCAL_CONFIG_PATH - # points to it, so we can ignore it. + # Make sure user has access to the path, and if the path is a + # link it's likely that another entry in SYSTEM_CONFIG_PATHS + # or LOCAL_CONFIG_PATH points to it, so we can ignore it. + path = _root(path, root) if os.access(path, os.R_OK) and not os.path.islink(path): for fullname in os.listdir(path): try: @@ -213,19 +224,20 @@ def _get_systemd_services(): return ret -def _get_sysv_services(systemd_services=None): +def _get_sysv_services(root, systemd_services=None): ''' Use os.listdir() and os.access() to get all the initscripts ''' + initscript_path = _root(INITSCRIPT_PATH, root) try: - sysv_services = os.listdir(INITSCRIPT_PATH) + sysv_services = os.listdir(initscript_path) except OSError as exc: if exc.errno == errno.ENOENT: pass elif exc.errno == errno.EACCES: log.error( 'Unable to check sysvinit scripts, permission denied to %s', - INITSCRIPT_PATH + initscript_path ) else: log.error( @@ -236,11 +248,11 @@ def _get_sysv_services(systemd_services=None): return [] if systemd_services is None: - systemd_services = _get_systemd_services() + systemd_services = _get_systemd_services(root) ret = [] for sysv_service in sysv_services: - if os.access(os.path.join(INITSCRIPT_PATH, sysv_service), os.X_OK): + if os.access(os.path.join(initscript_path, sysv_service), os.X_OK): if sysv_service in systemd_services: log.debug( 'sysvinit script \'%s\' found, but systemd unit ' @@ -303,7 +315,8 @@ def _strip_scope(msg): return '\n'.join(ret).strip() -def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False): +def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False, + root=None): ''' Build a systemctl command line. Treat unit names without one of the valid suffixes as a service. @@ -316,6 +329,8 @@ def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False): ret.append('systemctl') if no_block: ret.append('--no-block') + if root: + ret.extend(['--root', root]) if isinstance(action, six.string_types): action = shlex.split(action) ret.extend(action) @@ -343,26 +358,27 @@ def _systemctl_status(name): return __context__[contextkey] -def _sysv_enabled(name): +def _sysv_enabled(name, root): ''' A System-V style service is assumed disabled if the "startup" symlink (starts with "S") to its script is found in /etc/init.d in the current runlevel. ''' # Find exact match (disambiguate matches like "S01anacron" for cron) - for match in glob.glob('/etc/rc%s.d/S*%s' % (_runlevel(), name)): + rc = _root('/etc/rc{}.d/S*{}'.format(_runlevel(), name), root) + for match in glob.glob(rc): if re.match(r'S\d{,2}%s' % name, os.path.basename(match)): return True return False -def _untracked_custom_unit_found(name): +def _untracked_custom_unit_found(name, root=None): ''' If the passed service name is not available, but a unit file exist in /etc/systemd/system, return True. Otherwise, return False. ''' - unit_path = os.path.join('/etc/systemd/system', - _canonical_unit_name(name)) + system = _root('/etc/systemd/system', root) + unit_path = os.path.join(system, _canonical_unit_name(name)) return os.access(unit_path, os.R_OK) and not _check_available(name) @@ -371,7 +387,8 @@ def _unit_file_changed(name): Returns True if systemctl reports that the unit file has changed, otherwise returns False. ''' - return "'systemctl daemon-reload'" in _systemctl_status(name)['stdout'].lower() + status = _systemctl_status(name)['stdout'].lower() + return "'systemctl daemon-reload'" in status def systemctl_reload(): @@ -389,8 +406,7 @@ def systemctl_reload(): out = __salt__['cmd.run_all']( _systemctl_cmd('--system daemon-reload'), python_shell=False, - redirect_stderr=True - ) + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError( 'Problem performing systemctl daemon-reload: %s' % out['stdout'] @@ -414,8 +430,7 @@ def get_running(): out = __salt__['cmd.run']( _systemctl_cmd('--full --no-legend --no-pager'), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: comps = line.strip().split() @@ -438,10 +453,13 @@ def get_running(): return sorted(ret) -def get_enabled(): +def get_enabled(root=None): ''' Return a list of all enabled services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -452,10 +470,10 @@ def get_enabled(): # Get enabled systemd units. Can't use --state=enabled here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -473,15 +491,18 @@ def get_enabled(): # Add in any sysvinit services that are enabled ret.update(set( - [x for x in _get_sysv_services() if _sysv_enabled(x)] + [x for x in _get_sysv_services(root) if _sysv_enabled(x, root)] )) return sorted(ret) -def get_disabled(): +def get_disabled(root=None): ''' Return a list of all disabled services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -492,10 +513,10 @@ def get_disabled(): # Get disabled systemd units. Can't use --state=disabled here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -513,17 +534,20 @@ def get_disabled(): # Add in any sysvinit services that are disabled ret.update(set( - [x for x in _get_sysv_services() if not _sysv_enabled(x)] + [x for x in _get_sysv_services(root) if not _sysv_enabled(x, root)] )) return sorted(ret) -def get_static(): +def get_static(root=None): ''' .. versionadded:: 2015.8.5 Return a list of all static services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -534,10 +558,10 @@ def get_static(): # Get static systemd units. Can't use --state=static here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -557,18 +581,21 @@ def get_static(): return sorted(ret) -def get_all(): +def get_all(root=None): ''' Return a list of all available services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash salt '*' service.get_all ''' - ret = _get_systemd_services() - ret.update(set(_get_sysv_services(systemd_services=ret))) + ret = _get_systemd_services(root) + ret.update(set(_get_sysv_services(root, systemd_services=ret))) return sorted(ret) @@ -606,7 +633,7 @@ def missing(name): return not available(name) -def unmask_(name, runtime=False): +def unmask_(name, runtime=False, root=None): ''' .. versionadded:: 2015.5.0 .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -633,6 +660,9 @@ def unmask_(name, runtime=False): removes a runtime mask only when this argument is set to ``True``, and otherwise removes an indefinite mask. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -641,15 +671,16 @@ def unmask_(name, runtime=False): salt '*' service.unmask foo runtime=True ''' _check_for_unit_changes(name) - if not masked(name, runtime): + if not masked(name, runtime, root=root): log.debug('Service \'%s\' is not %smasked', name, 'runtime-' if runtime else '') return True cmd = 'unmask --runtime' if runtime else 'unmask' - out = __salt__['cmd.run_all'](_systemctl_cmd(cmd, name, systemd_scope=True), - python_shell=False, - redirect_stderr=True) + out = __salt__['cmd.run_all']( + _systemctl_cmd(cmd, name, systemd_scope=True, root=root), + python_shell=False, + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError('Failed to unmask service \'%s\'' % name) @@ -657,7 +688,7 @@ def unmask_(name, runtime=False): return True -def mask(name, runtime=False): +def mask(name, runtime=False, root=None): ''' .. versionadded:: 2015.5.0 .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -678,6 +709,9 @@ def mask(name, runtime=False): .. versionadded:: 2015.8.5 + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -688,9 +722,10 @@ def mask(name, runtime=False): _check_for_unit_changes(name) cmd = 'mask --runtime' if runtime else 'mask' - out = __salt__['cmd.run_all'](_systemctl_cmd(cmd, name, systemd_scope=True), - python_shell=False, - redirect_stderr=True) + out = __salt__['cmd.run_all']( + _systemctl_cmd(cmd, name, systemd_scope=True, root=root), + python_shell=False, + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError( @@ -701,7 +736,7 @@ def mask(name, runtime=False): return True -def masked(name, runtime=False): +def masked(name, runtime=False, root=None): ''' .. versionadded:: 2015.8.0 .. versionchanged:: 2015.8.5 @@ -731,6 +766,9 @@ def masked(name, runtime=False): only checks for runtime masks if this argument is set to ``True``. Otherwise, it will check for an indefinite mask. + root + Enable/disable/mask unit files in the specified root directory + CLI Examples: .. code-block:: bash @@ -739,7 +777,7 @@ def masked(name, runtime=False): salt '*' service.masked foo runtime=True ''' _check_for_unit_changes(name) - root_dir = '/run' if runtime else '/etc' + root_dir = _root('/run' if runtime else '/etc', root) link_path = os.path.join(root_dir, 'systemd', 'system', @@ -1055,9 +1093,10 @@ def status(name, sig=None): # pylint: disable=unused-argument results = {} for service in services: _check_for_unit_changes(service) - results[service] = __salt__['cmd.retcode'](_systemctl_cmd('is-active', service), - python_shell=False, - ignore_retcode=True) == 0 + results[service] = __salt__['cmd.retcode']( + _systemctl_cmd('is-active', service), + python_shell=False, + ignore_retcode=True) == 0 if contains_globbing: return results return results[name] @@ -1065,7 +1104,8 @@ def status(name, sig=None): # pylint: disable=unused-argument # **kwargs is required to maintain consistency with the API established by # Salt's service management states. -def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): # pylint: disable=unused-argument +def enable(name, no_block=False, unmask=False, unmask_runtime=False, + root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1101,6 +1141,9 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): In previous releases, Salt would simply unmask a service before enabling. This behavior is no longer the default. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1108,8 +1151,8 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): salt '*' service.enable ''' _check_for_unit_changes(name) - _check_unmask(name, unmask, unmask_runtime) - if name in _get_sysv_services(): + _check_unmask(name, unmask, unmask_runtime, root) + if name in _get_sysv_services(root): cmd = [] if salt.utils.systemd.has_scope(__context__) \ and __salt__['config.get']('systemd.scope', True): @@ -1123,7 +1166,8 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): python_shell=False, ignore_retcode=True) == 0 ret = __salt__['cmd.run_all']( - _systemctl_cmd('enable', name, systemd_scope=True, no_block=no_block), + _systemctl_cmd('enable', name, systemd_scope=True, no_block=no_block, + root=root), python_shell=False, ignore_retcode=True) @@ -1137,7 +1181,7 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): # The unused kwargs argument is required to maintain consistency with the API # established by Salt's service management states. -def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument +def disable(name, no_block=False, root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1157,6 +1201,9 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument .. versionadded:: 2017.7.0 + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1164,7 +1211,7 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument salt '*' service.disable ''' _check_for_unit_changes(name) - if name in _get_sysv_services(): + if name in _get_sysv_services(root): cmd = [] if salt.utils.systemd.has_scope(__context__) \ and __salt__['config.get']('systemd.scope', True): @@ -1179,17 +1226,21 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument ignore_retcode=True) == 0 # Using cmd.run_all instead of cmd.retcode here to make unit tests easier return __salt__['cmd.run_all']( - _systemctl_cmd('disable', name, systemd_scope=True, no_block=no_block), + _systemctl_cmd('disable', name, systemd_scope=True, no_block=no_block, + root=root), python_shell=False, ignore_retcode=True)['retcode'] == 0 # The unused kwargs argument is required to maintain consistency with the API # established by Salt's service management states. -def enabled(name, **kwargs): # pylint: disable=unused-argument +def enabled(name, root=None, **kwargs): # pylint: disable=unused-argument ''' Return if the named service is enabled to start on boot + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1199,7 +1250,7 @@ def enabled(name, **kwargs): # pylint: disable=unused-argument # Try 'systemctl is-enabled' first, then look for a symlink created by # systemctl (older systemd releases did not support using is-enabled to # check templated services), and lastly check for a sysvinit service. - if __salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name), + if __salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name, root=root), python_shell=False, ignore_retcode=True) == 0: return True @@ -1207,43 +1258,50 @@ def enabled(name, **kwargs): # pylint: disable=unused-argument # On older systemd releases, templated services could not be checked # with ``systemctl is-enabled``. As a fallback, look for the symlinks # created by systemctl when enabling templated services. - cmd = ['find', LOCAL_CONFIG_PATH, '-name', name, + local_config_path = _root(LOCAL_CONFIG_PATH, '/') + cmd = ['find', local_config_path, '-name', name, '-type', 'l', '-print', '-quit'] # If the find command returns any matches, there will be output and the # string will be non-empty. if bool(__salt__['cmd.run'](cmd, python_shell=False)): return True - elif name in _get_sysv_services(): - return _sysv_enabled(name) + elif name in _get_sysv_services(root): + return _sysv_enabled(name, root) return False -def disabled(name): +def disabled(name, root=None): ''' Return if the named service is disabled from starting on boot + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash salt '*' service.disabled ''' - return not enabled(name) + return not enabled(name, root=root) -def show(name): +def show(name, root=None): ''' .. versionadded:: 2014.7.0 Show properties of one or more units/jobs or the manager + root + Enable/disable/mask unit files in the specified root directory + CLI Example: salt '*' service.show ''' ret = {} - out = __salt__['cmd.run'](_systemctl_cmd('show', name), + out = __salt__['cmd.run'](_systemctl_cmd('show', name, root=root), python_shell=False) for line in salt.utils.itertools.split(out, '\n'): comps = line.split('=') @@ -1263,19 +1321,22 @@ def show(name): return ret -def execs(): +def execs(root=None): ''' .. versionadded:: 2014.7.0 Return a list of all files specified as ``ExecStart`` for all services. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: salt '*' service.execs ''' ret = {} - for service in get_all(): - data = show(service) + for service in get_all(root=root): + data = show(service, root=root) if 'ExecStart' not in data: continue ret[service] = data['ExecStart']['path'] diff --git a/tests/unit/modules/test_systemd_service.py b/tests/unit/modules/test_systemd_service.py index 1d3a760c1315..deeb23591b72 100644 --- a/tests/unit/modules/test_systemd_service.py +++ b/tests/unit/modules/test_systemd_service.py @@ -110,7 +110,7 @@ def test_get_enabled(self): 'README' ) ) - sysv_enabled_mock = MagicMock(side_effect=lambda x: x == 'baz') + sysv_enabled_mock = MagicMock(side_effect=lambda x, _: x == 'baz') with patch.dict(systemd.__salt__, {'cmd.run': cmd_mock}): with patch.object(os, 'listdir', listdir_mock): @@ -146,7 +146,7 @@ def test_get_disabled(self): 'README' ) ) - sysv_enabled_mock = MagicMock(side_effect=lambda x: x == 'baz') + sysv_enabled_mock = MagicMock(side_effect=lambda x, _: x == 'baz') with patch.dict(systemd.__salt__, {'cmd.run': cmd_mock}): with patch.object(os, 'listdir', listdir_mock):