You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
9.5 KiB
267 lines
9.5 KiB
9 months ago
|
import fnmatch
|
||
|
import os
|
||
|
|
||
|
from leapp.libraries.stdlib import api, CalledProcessError, run
|
||
|
from leapp.models import SystemdServiceFile, SystemdServicePreset
|
||
|
|
||
|
SYSTEMD_SYMLINKS_DIR = '/etc/systemd/system/'
|
||
|
|
||
|
_SYSTEMCTL_CMD_OPTIONS = ['--type=service', '--all', '--plain', '--no-legend']
|
||
|
_USR_PRESETS_PATH = '/usr/lib/systemd/system-preset/'
|
||
|
_ETC_PRESETS_PATH = '/etc/systemd/system-preset/'
|
||
|
|
||
|
SYSTEMD_SYSTEM_LOAD_PATH = [
|
||
|
'/etc/systemd/system',
|
||
|
'/usr/lib/systemd/system'
|
||
|
]
|
||
|
|
||
|
|
||
|
def get_broken_symlinks():
|
||
|
"""
|
||
|
Get broken systemd symlinks on the system
|
||
|
|
||
|
:return: List of broken systemd symlinks
|
||
|
:rtype: list[str]
|
||
|
:raises: CalledProcessError: if the `find` command fails
|
||
|
:raises: OSError: if the find utility is not found
|
||
|
"""
|
||
|
try:
|
||
|
return run(['find', SYSTEMD_SYMLINKS_DIR, '-xtype', 'l'], split=True)['stdout']
|
||
|
except (OSError, CalledProcessError):
|
||
|
api.current_logger().error('Cannot obtain the list of broken systemd symlinks.')
|
||
|
raise
|
||
|
|
||
|
|
||
|
def _try_call_unit_command(command, unit):
|
||
|
try:
|
||
|
# it is possible to call this on multiple units at once,
|
||
|
# but failing to enable one service would cause others to not enable as well
|
||
|
run(['systemctl', command, unit])
|
||
|
except CalledProcessError as err:
|
||
|
msg = 'Failed to {} systemd unit "{}". Message: {}'.format(command, unit, str(err))
|
||
|
api.current_logger().error(msg)
|
||
|
raise err
|
||
|
|
||
|
|
||
|
def enable_unit(unit):
|
||
|
"""
|
||
|
Enable a systemd unit
|
||
|
|
||
|
It is strongly recommended to produce SystemdServicesTasks message instead,
|
||
|
unless it is absolutely necessary to handle failure yourself.
|
||
|
|
||
|
:param unit: The systemd unit to enable
|
||
|
:raises CalledProcessError: In case of failure
|
||
|
"""
|
||
|
_try_call_unit_command('enable', unit)
|
||
|
|
||
|
|
||
|
def disable_unit(unit):
|
||
|
"""
|
||
|
Disable a systemd unit
|
||
|
|
||
|
It is strongly recommended to produce SystemdServicesTasks message instead,
|
||
|
unless it is absolutely necessary to handle failure yourself.
|
||
|
|
||
|
:param unit: The systemd unit to disable
|
||
|
:raises CalledProcessError: In case of failure
|
||
|
"""
|
||
|
_try_call_unit_command('disable', unit)
|
||
|
|
||
|
|
||
|
def reenable_unit(unit):
|
||
|
"""
|
||
|
Re-enable a systemd unit
|
||
|
|
||
|
It is strongly recommended to produce SystemdServicesTasks message, unless it
|
||
|
is absolutely necessary to handle failure yourself.
|
||
|
|
||
|
:param unit: The systemd unit to re-enable
|
||
|
:raises CalledProcessError: In case of failure
|
||
|
"""
|
||
|
_try_call_unit_command('reenable', unit)
|
||
|
|
||
|
|
||
|
def get_service_files():
|
||
|
"""
|
||
|
Get list of unit files of systemd services on the system
|
||
|
|
||
|
The list includes template units.
|
||
|
|
||
|
:return: List of service unit files with states
|
||
|
:rtype: list[SystemdServiceFile]
|
||
|
:raises: CalledProcessError: in case of failure of `systemctl` command
|
||
|
"""
|
||
|
services_files = []
|
||
|
try:
|
||
|
cmd = ['systemctl', 'list-unit-files'] + _SYSTEMCTL_CMD_OPTIONS
|
||
|
service_units_data = run(cmd, split=True)['stdout']
|
||
|
except CalledProcessError as err:
|
||
|
api.current_logger().error('Cannot obtain the list of unit files:{}'.format(str(err)))
|
||
|
raise
|
||
|
|
||
|
for entry in service_units_data:
|
||
|
columns = entry.split()
|
||
|
services_files.append(SystemdServiceFile(name=columns[0], state=columns[1]))
|
||
|
return services_files
|
||
|
|
||
|
|
||
|
def _join_presets_resolving_overrides(etc_files, usr_files):
|
||
|
"""
|
||
|
Join presets and resolve preset file overrides
|
||
|
|
||
|
Preset files in /etc/ override those with the same name in /usr/.
|
||
|
If such a file is a symlink to /dev/null, it disables the one in /usr/ instead.
|
||
|
|
||
|
:param etc_files: Systemd preset files in /etc/
|
||
|
:param usr_files: Systemd preset files in /usr/
|
||
|
:return: List of preset files in /etc/ and /usr/ with overridden files removed
|
||
|
"""
|
||
|
for etc_file in etc_files:
|
||
|
filename = os.path.basename(etc_file)
|
||
|
for usr_file in usr_files:
|
||
|
if filename == os.path.basename(usr_file):
|
||
|
usr_files.remove(usr_file)
|
||
|
if os.path.islink(etc_file) and os.readlink(etc_file) == '/dev/null':
|
||
|
etc_files.remove(etc_file)
|
||
|
|
||
|
return etc_files + usr_files
|
||
|
|
||
|
|
||
|
def _search_preset_files(path):
|
||
|
"""
|
||
|
Search preset files in the given path
|
||
|
|
||
|
Presets are search recursively in the given directory.
|
||
|
If path isn't an existing directory, return empty list.
|
||
|
|
||
|
:param path: The path to search preset files in
|
||
|
:return: List of found preset files
|
||
|
:rtype: list[str]
|
||
|
:raises: CalledProcessError: if the `find` command fails
|
||
|
:raises: OSError: if the find utility is not found
|
||
|
"""
|
||
|
if os.path.isdir(path):
|
||
|
try:
|
||
|
return run(['find', path, '-name', '*.preset'], split=True)['stdout']
|
||
|
except (OSError, CalledProcessError) as err:
|
||
|
api.current_logger().error('Cannot obtain list of systemd preset files in {}:{}'.format(path, str(err)))
|
||
|
raise
|
||
|
else:
|
||
|
return []
|
||
|
|
||
|
|
||
|
def _get_system_preset_files():
|
||
|
"""
|
||
|
Get systemd system preset files and remove overriding entries. Entries in /run/systemd/system are ignored.
|
||
|
|
||
|
:return: List of system systemd preset files
|
||
|
:raises: CalledProcessError: if the `find` command fails
|
||
|
:raises: OSError: if the find utility is not found
|
||
|
"""
|
||
|
etc_files = _search_preset_files(_ETC_PRESETS_PATH)
|
||
|
usr_files = _search_preset_files(_USR_PRESETS_PATH)
|
||
|
|
||
|
preset_files = _join_presets_resolving_overrides(etc_files, usr_files)
|
||
|
preset_files.sort()
|
||
|
return preset_files
|
||
|
|
||
|
|
||
|
def _recursive_glob(pattern, root_dir):
|
||
|
for _, _, filenames in os.walk(root_dir):
|
||
|
for filename in filenames:
|
||
|
if fnmatch.fnmatch(filename, pattern):
|
||
|
yield filename
|
||
|
|
||
|
|
||
|
def _parse_preset_entry(entry, presets, load_path):
|
||
|
"""
|
||
|
Parse a single entry (line) in a preset file
|
||
|
|
||
|
Single entry might set presets on multiple units using globs.
|
||
|
|
||
|
:param entry: The entry to parse
|
||
|
:param presets: Dictionary to store the presets into
|
||
|
:param load_path: List of paths to look systemd unit files up in
|
||
|
"""
|
||
|
|
||
|
columns = entry.split()
|
||
|
if len(columns) < 2 or columns[0] not in ('enable', 'disable'):
|
||
|
raise ValueError('Invalid preset file entry: "{}"'.format(entry))
|
||
|
|
||
|
for path in load_path:
|
||
|
# TODO(mmatuska): This currently also globs non unit files,
|
||
|
# so the results need to be filtered with something like endswith('.<unit_type>')
|
||
|
unit_files = _recursive_glob(columns[1], root_dir=path)
|
||
|
|
||
|
for unit_file in unit_files:
|
||
|
if '@' in columns[1] and len(columns) > 2:
|
||
|
# unit is a template,
|
||
|
# if the entry contains instance names after template unit name
|
||
|
# the entry only applies to the specified instances, not to the
|
||
|
# template itself
|
||
|
for instance in columns[2:]:
|
||
|
service_name = unit_file[:unit_file.index('@') + 1] + instance + '.service'
|
||
|
if service_name not in presets: # first occurrence has priority
|
||
|
presets[service_name] = columns[0]
|
||
|
|
||
|
elif unit_file not in presets: # first occurrence has priority
|
||
|
presets[unit_file] = columns[0]
|
||
|
|
||
|
|
||
|
def _parse_preset_files(preset_files, load_path, ignore_invalid_entries):
|
||
|
"""
|
||
|
Parse presets from preset files
|
||
|
|
||
|
:param load_path: List of paths to search units at
|
||
|
:param ignore_invalid_entries: Whether to ignore invalid entries in preset files or raise an error
|
||
|
:return: Dictionary mapping systemd units to their preset state
|
||
|
:rtype: dict[str, str]
|
||
|
:raises: ValueError: when a preset file has invalid content
|
||
|
"""
|
||
|
presets = {}
|
||
|
|
||
|
for preset in preset_files:
|
||
|
with open(preset, 'r') as preset_file:
|
||
|
for line in preset_file:
|
||
|
stripped = line.strip()
|
||
|
if stripped and stripped[0] not in ('#', ';'): # ignore comments
|
||
|
try:
|
||
|
_parse_preset_entry(stripped, presets, load_path)
|
||
|
except ValueError as err:
|
||
|
new_msg = 'Invalid preset file {pfile}: {error}'.format(pfile=preset, error=str(err))
|
||
|
if ignore_invalid_entries:
|
||
|
api.current_logger().warning(new_msg)
|
||
|
continue
|
||
|
raise ValueError(new_msg)
|
||
|
return presets
|
||
|
|
||
|
|
||
|
def get_system_service_preset_files(service_files, ignore_invalid_entries=False):
|
||
|
"""
|
||
|
Get system preset files for services
|
||
|
|
||
|
Presets for static and transient services are filtered out.
|
||
|
|
||
|
:param services_files: List of service unit files
|
||
|
:param ignore_invalid_entries: Ignore invalid entries in preset files if True, raise ValueError otherwise
|
||
|
:return: List of system systemd services presets
|
||
|
:rtype: list[SystemdServicePreset]
|
||
|
:raises: CalledProcessError: In case of errors when discovering systemd preset files
|
||
|
:raises: OSError: When the `find` command is not available
|
||
|
:raises: ValueError: When a preset file has invalid content and ignore_invalid_entries is False
|
||
|
"""
|
||
|
preset_files = _get_system_preset_files()
|
||
|
presets = _parse_preset_files(preset_files, SYSTEMD_SYSTEM_LOAD_PATH, ignore_invalid_entries)
|
||
|
|
||
|
preset_models = []
|
||
|
for unit, state in presets.items():
|
||
|
if unit.endswith('.service'):
|
||
|
service_file = next(iter([s for s in service_files if s.name == unit]), None)
|
||
|
# presets can also be set on instances of template services which don't have a unit file
|
||
|
if service_file and service_file.state in ('static', 'transient'):
|
||
|
continue
|
||
|
preset_models.append(SystemdServicePreset(service=unit, state=state))
|
||
|
|
||
|
return preset_models
|