-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add systemd_info module * fix object results * apply review changes * apply module change and add doc_fragments * removed use_unsafe_shell and doc_fragments/systemd * fix unitname description doc * fixed doc, replaced systemctl show syntax, added base prop result doc * fix documentation * fix RV values in description * fix RV() description values * add get_bin_path try/fail and remove list() * fix doc, removed try block * add Archlinux in integration test
- Loading branch information
1 parent
2b6f4ba
commit 8425464
Showing
5 changed files
with
506 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,361 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2025, Marco Noce <nce.marco@gmail.com> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import absolute_import, division, print_function | ||
__metaclass__ = type | ||
|
||
DOCUMENTATION = r''' | ||
--- | ||
module: systemd_info | ||
short_description: Gather C(systemd) unit info | ||
description: | ||
- This module gathers info about systemd units (services, targets, sockets, mount). | ||
- It runs C(systemctl list-units) (or processes selected units) and collects properties | ||
for each unit using C(systemctl show). | ||
- Even if a unit has a RV(units.loadstate) of V(not-found) or V(masked), it is returned, | ||
but only with the minimal properties (RV(units.name), RV(units.loadstate), RV(units.activestate), RV(units.substate)). | ||
- When O(unitname) and O(extra_properties) are used, the module first checks if the unit exists, | ||
then check if properties exist. If not, the module fails. | ||
version_added: "10.4.0" | ||
options: | ||
unitname: | ||
description: | ||
- List of unit names to process. | ||
- It supports C(.service), C(.target), C(.socket), and C(.mount) units type. | ||
- Each name must correspond to the full name of the C(systemd) unit. | ||
type: list | ||
elements: str | ||
default: [] | ||
extra_properties: | ||
description: | ||
- Additional properties to retrieve (appended to the default ones). | ||
- Note that all property names are converted to lower-case. | ||
type: list | ||
elements: str | ||
default: [] | ||
author: | ||
- Marco Noce (@NomakCooper) | ||
extends_documentation_fragment: | ||
- community.general.attributes | ||
- community.general.attributes.info_module | ||
''' | ||
|
||
EXAMPLES = r''' | ||
--- | ||
# Gather info for all systemd services, targets, sockets and mount | ||
- name: Gather all systemd unit info | ||
community.general.systemd_info: | ||
register: results | ||
# Gather info for selected units with extra properties. | ||
- name: Gather info for selected unit(s) | ||
community.general.systemd_info: | ||
unitname: | ||
- systemd-journald.service | ||
- systemd-journald.socket | ||
- sshd-keygen.target | ||
- -.mount | ||
extra_properties: | ||
- Description | ||
register: results | ||
''' | ||
|
||
RETURN = r''' | ||
units: | ||
description: | ||
- Dictionary of systemd unit info keyed by unit name. | ||
- Additional fields will be returned depending on the value of O(extra_properties). | ||
returned: success | ||
type: dict | ||
elements: dict | ||
contains: | ||
name: | ||
description: Unit full name. | ||
returned: always | ||
type: str | ||
sample: systemd-journald.service | ||
loadstate: | ||
description: | ||
- The state of the unit's configuration load. | ||
- The most common values are V(loaded), V(not-found), and V(masked), but other values are possible as well. | ||
returned: always | ||
type: str | ||
sample: loaded | ||
activestate: | ||
description: | ||
- The current active state of the unit. | ||
- The most common values are V(active), V(inactive), and V(failed), but other values are possible as well. | ||
returned: always | ||
type: str | ||
sample: active | ||
substate: | ||
description: | ||
- The detailed sub state of the unit. | ||
- The most common values are V(running), V(dead), V(exited), V(failed), V(listening), V(active), and V(mounted), but other values are possible as well. | ||
returned: always | ||
type: str | ||
sample: running | ||
fragmentpath: | ||
description: Path to the unit's fragment file. | ||
returned: always except for C(.mount) units. | ||
type: str | ||
sample: /usr/lib/systemd/system/systemd-journald.service | ||
unitfilepreset: | ||
description: | ||
- The preset configuration state for the unit file. | ||
- The most common values are V(enabled), V(disabled), and V(static), but other values are possible as well. | ||
returned: always except for C(.mount) units. | ||
type: str | ||
sample: disabled | ||
unitfilestate: | ||
description: | ||
- The actual configuration state for the unit file. | ||
- The most common values are V(enabled), V(disabled), and V(static), but other values are possible as well. | ||
returned: always except for C(.mount) units. | ||
type: str | ||
sample: enabled | ||
mainpid: | ||
description: PID of the main process of the unit. | ||
returned: only for C(.service) units. | ||
type: str | ||
sample: 798 | ||
execmainpid: | ||
description: PID of the ExecStart process of the unit. | ||
returned: only for C(.service) units. | ||
type: str | ||
sample: 799 | ||
options: | ||
description: The mount options. | ||
returned: only for C(.mount) units. | ||
type: str | ||
sample: rw,relatime,noquota | ||
type: | ||
description: The filesystem type of the mounted device. | ||
returned: only for C(.mount) units. | ||
type: str | ||
sample: ext4 | ||
what: | ||
description: The device that is mounted. | ||
returned: only for C(.mount) units. | ||
type: str | ||
sample: /dev/sda1 | ||
where: | ||
description: The mount point where the device is mounted. | ||
returned: only for C(.mount) units. | ||
type: str | ||
sample: / | ||
sample: { | ||
"-.mount": { | ||
"activestate": "active", | ||
"description": "Root Mount", | ||
"loadstate": "loaded", | ||
"name": "-.mount", | ||
"options": "rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota", | ||
"substate": "mounted", | ||
"type": "xfs", | ||
"what": "/dev/mapper/cs-root", | ||
"where": "/" | ||
}, | ||
"sshd-keygen.target": { | ||
"activestate": "active", | ||
"description": "sshd-keygen.target", | ||
"fragmentpath": "/usr/lib/systemd/system/sshd-keygen.target", | ||
"loadstate": "loaded", | ||
"name": "sshd-keygen.target", | ||
"substate": "active", | ||
"unitfilepreset": "disabled", | ||
"unitfilestate": "static" | ||
}, | ||
"systemd-journald.service": { | ||
"activestate": "active", | ||
"description": "Journal Service", | ||
"execmainpid": "613", | ||
"fragmentpath": "/usr/lib/systemd/system/systemd-journald.service", | ||
"loadstate": "loaded", | ||
"mainpid": "613", | ||
"name": "systemd-journald.service", | ||
"substate": "running", | ||
"unitfilepreset": "disabled", | ||
"unitfilestate": "static" | ||
}, | ||
"systemd-journald.socket": { | ||
"activestate": "active", | ||
"description": "Journal Socket", | ||
"fragmentpath": "/usr/lib/systemd/system/systemd-journald.socket", | ||
"loadstate": "loaded", | ||
"name": "systemd-journald.socket", | ||
"substate": "running", | ||
"unitfilepreset": "disabled", | ||
"unitfilestate": "static" | ||
} | ||
} | ||
''' | ||
|
||
from ansible.module_utils.basic import AnsibleModule | ||
|
||
|
||
def run_command(module, cmd): | ||
rc, stdout, stderr = module.run_command(cmd, check_rc=True) | ||
return stdout.strip() | ||
|
||
|
||
def parse_show_output(output): | ||
result = {} | ||
for line in output.splitlines(): | ||
if "=" in line: | ||
key, val = line.split("=", 1) | ||
key = key.lower() | ||
if key not in result: | ||
result[key] = val | ||
return result | ||
|
||
|
||
def get_unit_properties(module, systemctl_bin, unit, prop_list): | ||
cmd = [systemctl_bin, "show", "-p", ",".join(prop_list), "--", unit] | ||
output = run_command(module, cmd) | ||
return parse_show_output(output) | ||
|
||
|
||
def determine_category(unit): | ||
if unit.endswith('.service'): | ||
return 'service' | ||
elif unit.endswith('.target'): | ||
return 'target' | ||
elif unit.endswith('.socket'): | ||
return 'socket' | ||
elif unit.endswith('.mount'): | ||
return 'mount' | ||
else: | ||
return None | ||
|
||
|
||
def extract_unit_properties(unit_data, prop_list): | ||
lowerprop = [x.lower() for x in prop_list] | ||
extracted = { | ||
prop: unit_data[prop] for prop in lowerprop if prop in unit_data | ||
} | ||
return extracted | ||
|
||
|
||
def unit_exists(module, systemctl_bin, unit): | ||
cmd = [systemctl_bin, "show", "-p", "LoadState", "--", unit] | ||
rc, stdout, stderr = module.run_command(cmd) | ||
return (rc == 0) | ||
|
||
|
||
def validate_unit_and_properties(module, systemctl_bin, unit, extra_properties): | ||
cmd = [systemctl_bin, "show", "-p", "LoadState", "--", unit] | ||
|
||
output = run_command(module, cmd) | ||
if "loadstate=not-found" in output.lower(): | ||
module.fail_json(msg="Unit '{0}' does not exist or is inaccessible.".format(unit)) | ||
|
||
if extra_properties: | ||
unit_data = get_unit_properties(module, systemctl_bin, unit, extra_properties) | ||
missing_props = [prop for prop in extra_properties if prop.lower() not in unit_data] | ||
if missing_props: | ||
module.fail_json(msg="The following properties do not exist for unit '{0}': {1}".format(unit, ", ".join(missing_props))) | ||
|
||
|
||
def main(): | ||
module_args = dict( | ||
unitname=dict(type='list', elements='str', default=[]), | ||
extra_properties=dict(type='list', elements='str', default=[]) | ||
) | ||
|
||
module = AnsibleModule( | ||
argument_spec=module_args, | ||
supports_check_mode=True | ||
) | ||
|
||
systemctl_bin = module.get_bin_path('systemctl', required=True) | ||
|
||
run_command(module, [systemctl_bin, '--version']) | ||
|
||
base_properties = { | ||
'service': ['FragmentPath', 'UnitFileState', 'UnitFilePreset', 'MainPID', 'ExecMainPID'], | ||
'target': ['FragmentPath', 'UnitFileState', 'UnitFilePreset'], | ||
'socket': ['FragmentPath', 'UnitFileState', 'UnitFilePreset'], | ||
'mount': ['Where', 'What', 'Options', 'Type'] | ||
} | ||
state_props = ['LoadState', 'ActiveState', 'SubState'] | ||
|
||
results = {} | ||
|
||
if not module.params['unitname']: | ||
list_cmd = [ | ||
systemctl_bin, "list-units", | ||
"--no-pager", | ||
"--type", "service,target,socket,mount", | ||
"--all", | ||
"--plain", | ||
"--no-legend" | ||
] | ||
list_output = run_command(module, list_cmd) | ||
for line in list_output.splitlines(): | ||
tokens = line.split() | ||
if len(tokens) < 4: | ||
continue | ||
|
||
unit_name = tokens[0] | ||
loadstate = tokens[1] | ||
activestate = tokens[2] | ||
substate = tokens[3] | ||
|
||
fact = { | ||
"name": unit_name, | ||
"loadstate": loadstate, | ||
"activestate": activestate, | ||
"substate": substate | ||
} | ||
|
||
if loadstate in ("not-found", "masked"): | ||
results[unit_name] = fact | ||
continue | ||
|
||
category = determine_category(unit_name) | ||
if not category: | ||
results[unit_name] = fact | ||
continue | ||
|
||
props = base_properties.get(category, []) | ||
full_props = set(props + state_props) | ||
unit_data = get_unit_properties(module, systemctl_bin, unit_name, full_props) | ||
|
||
fact.update(extract_unit_properties(unit_data, full_props)) | ||
results[unit_name] = fact | ||
|
||
else: | ||
selected_units = module.params['unitname'] | ||
extra_properties = module.params['extra_properties'] | ||
|
||
for unit in selected_units: | ||
validate_unit_and_properties(module, systemctl_bin, unit, extra_properties) | ||
category = determine_category(unit) | ||
|
||
if not category: | ||
module.fail_json(msg="Could not determine the category for unit '{0}'.".format(unit)) | ||
|
||
props = base_properties.get(category, []) | ||
full_props = set(props + state_props + extra_properties) | ||
unit_data = get_unit_properties(module, systemctl_bin, unit, full_props) | ||
fact = {"name": unit} | ||
minimal_keys = ["LoadState", "ActiveState", "SubState"] | ||
|
||
fact.update(extract_unit_properties(unit_data, minimal_keys)) | ||
|
||
ls = unit_data.get("loadstate", "").lower() | ||
if ls not in ("not-found", "masked"): | ||
fact.update(extract_unit_properties(unit_data, full_props)) | ||
|
||
results[unit] = fact | ||
|
||
module.exit_json(changed=False, units=results) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
needs/root | ||
azp/posix/1 | ||
skip/aix | ||
skip/freebsd | ||
skip/osx | ||
skip/macos |
Oops, something went wrong.