Skip to content

Commit c295253

Browse files
committed
feat: add support for user units
Feature: The role can manage user units in addition to system units. Each item in each input list can be a string or a `dict` consisting of the item (file, template, or unit), a user name, and a state (for files and templates). The role will not create users and will give an error if a non-existent user is specified. Reason: The role should allow management of user units. Result: The role can manage user units. NOTE: User units only supported on EL8 and later. Signed-off-by: Rich Megginson <[email protected]>
1 parent 5aa54ad commit c295253

14 files changed

+830
-109
lines changed

README.md

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ wrapper around systemd and template Ansible Core modules.
99

1010
## Requirements
1111

12-
See below
12+
*NOTE:* Support for user units is not available in EL7 or earlier. This feature
13+
is only available in EL8 and later.
1314

1415
### Collection requirements
1516

@@ -24,6 +25,26 @@ ansible-galaxy collection install -vv -r meta/collection-requirements.yml
2425

2526
List of variables consumed by the role follows, note that none of them is mandatory.
2627

28+
Each of the variables can either be a list of strings, or a list of `dicts`.
29+
30+
The list of strings form assumes that the items to be managed are system units
31+
owned by `root`, and for files, assumes that the files should be `present`.
32+
33+
The list of `dict` form looks like this:
34+
35+
```yaml
36+
systemd_unit_files:
37+
- item: some.service
38+
user: my_user
39+
state: [present|absent]
40+
```
41+
42+
Use the `dict` form to manage user units, and to remove unit files. If using
43+
user units, the role will manage lingering for those users.
44+
45+
*NOTE:* Support for user units is not available in EL7 or earlier. This feature
46+
is only available in EL8 and later.
47+
2748
### systemd_unit_files
2849

2950
List of systemd unit file names that should be deployed to managed nodes.
@@ -79,7 +100,7 @@ List of unit files that shall be unmasked via systemd.
79100

80101
This variable is used to handle reboots required by transactional updates. If a transactional update requires a reboot, the role will proceed with the reboot if systemd_transactional_update_reboot_ok is set to true. If set to false, the role will notify the user that a reboot is required, allowing for custom handling of the reboot requirement. If this variable is not set, the role will fail to ensure the reboot requirement is not overlooked.
81102

82-
Example of setting the variables:
103+
Example of setting the variables for the simple list of strings format:
83104

84105
```yaml
85106
systemd_unit_files:
@@ -96,12 +117,49 @@ systemd_enabled_units:
96117
- bar.service
97118
```
98119

120+
Example of setting the variables for the list of `dict` format:
121+
122+
```yaml
123+
systemd_unit_files:
124+
- item: foo.service
125+
user: root
126+
state: present
127+
- item: bar.service
128+
user: my_user
129+
state: absent
130+
systemd_dropins:
131+
- item: cups.service.conf.j2
132+
user: root
133+
state: present
134+
- item: avahi-daemon.service.conf.j2
135+
user: my_user
136+
state: absent
137+
systemd_started_units:
138+
- item: foo.service
139+
user: root
140+
- item: bar.service
141+
user: my_user
142+
systemd_enabled_units:
143+
- item: foo.service
144+
user: root
145+
- item: bar.service
146+
user: my_user
147+
```
148+
99149
## Variables Exported by the Role
100150

101151
### `systemd_units`
102152

103-
Variable shall contain a list of dictionaries where each entry describes state of one systemd unit
104-
present on the managed host.
153+
The variable is a `dict`. Each key is the name of a systemd unit. Each value
154+
is a dict with fields that describe the state of that systemd unit present on
155+
the managed host for the system scope.
156+
157+
### `systemd_units_user`
158+
159+
Variable shall contain a dict. Each key is the name of a user given in one of
160+
the lists passed to the role, and `root` (even if `root` is not given). Each
161+
value is a dict of systemd units for that user, or system units for `root`, in
162+
the format of `systemd_units` above.
105163

106164
## Example Playbook
107165

@@ -112,7 +170,10 @@ present on the managed host.
112170
systemd_unit_file_templates:
113171
- foo.service.j2
114172
systemd_started_units:
115-
- foo.service
173+
- item: foo.service
174+
user: root
175+
- item: bar.service
176+
user: my_user
116177
systemd_enabled_units:
117178
- foo.service
118179
roles:
@@ -130,3 +191,4 @@ MIT
130191
## Author
131192

132193
Michal Sekletar <[email protected]>
194+
Rich Megginson <[email protected]>

defaults/main.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# SPDX-License-Identifier: MIT
22
---
3+
# These can either be lists of strings, which are the names of files, units, etc.
4+
# or a list of dict
5+
# the dict can have the following fields
6+
# item: name of file, template, unit, etc.
7+
# user: root for system units, or user for user units
8+
# if no user is given, root is used
9+
# state: default is present - use absent to remove unit files
310
systemd_unit_files: []
411
systemd_unit_file_templates: []
512
systemd_dropins: []
@@ -11,4 +18,10 @@ systemd_enabled_units: []
1118
systemd_disabled_units: []
1219
systemd_masked_units: []
1320
systemd_unmasked_units: []
21+
22+
# This only applies to transactional update systems
1423
systemd_transactional_update_reboot_ok: null
24+
25+
# fail the role if there is an attempt to use a feature
26+
# not supported by the version of systemd
27+
systemd_fail_if_too_old: true

library/systemd_units.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@
3131
requirements: [systemd]
3232
description:
3333
- This module gathers state of systemd units and exposes it as facts data
34-
options: {}
34+
options:
35+
user:
36+
description: user name
37+
type: str
38+
required: true
39+
user_facts:
40+
description: getent facts about user
41+
type: dict
42+
required: true
3543
"""
3644

3745
RETURN = r"""
@@ -97,10 +105,15 @@ def units(self):
97105
systemctl = self.module.get_bin_path(
98106
"systemctl", opt_dirs=["/usr/bin", "/usr/local/bin"]
99107
)
108+
if self.module.params["user"] == "root":
109+
flag = "--system"
110+
else:
111+
flag = "--user"
100112

101113
units = {}
102114
rc, stdout, stderr = self.module.run_command(
103-
"%s list-units --no-pager --no-legend" % systemctl, use_unsafe_shell=True
115+
"%s %s list-units --no-pager --no-legend" % (systemctl, flag),
116+
use_unsafe_shell=True,
104117
)
105118
if rc != 0:
106119
self.module.warn("Could not list units: %s" % stderr)
@@ -121,12 +134,24 @@ def units(self):
121134

122135

123136
def main():
124-
module = AnsibleModule(argument_spec={}, supports_check_mode=True)
137+
argument_spec = dict(
138+
user=dict(type="str", required=True),
139+
user_facts=dict(type="dict", required=True),
140+
)
141+
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
125142
systemd_facts_module = SystemdUnitsFacts(module)
126143

127144
units = systemd_facts_module.units()
128-
129-
results = dict(ansible_facts={"systemd_units": units})
145+
user = module.params["user"]
146+
user_facts = module.params["user_facts"]
147+
# module.log("user [%s] user_facts %s" % (user, user_facts))
148+
user_facts[user] = units
149+
facts = {"systemd_units_user": user_facts}
150+
if user == "root":
151+
# add the legacy fact
152+
facts.update({"systemd_units": units})
153+
154+
results = dict(ansible_facts=facts)
130155
module.exit_json(**results)
131156

132157

tasks/cancel_linger.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-License-Identifier: MIT
2+
# Inputs:
3+
# __systemd_user_info
4+
---
5+
- name: Check if linger for users can be cancelled
6+
vars:
7+
__systemd_linger_users: "{{ __systemd_user_info | dict2items |
8+
rejectattr('key', 'match', '^root$') | list }}"
9+
when: __systemd_linger_users | length > 0
10+
block:
11+
- name: Cancel linger for given user
12+
include_tasks: cancel_linger_for_user.yml
13+
vars:
14+
__systemd_linger_data: "{{ __systemd_linger_user_data.value }}"
15+
__systemd_linger_user: "{{ __systemd_linger_user_data.key }}"
16+
loop: "{{ __systemd_linger_users }}"
17+
loop_control:
18+
loop_var: __systemd_linger_user_data

tasks/cancel_linger_for_user.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: MIT
2+
# Inputs:
3+
# __systemd_linger_user_data
4+
---
5+
- name: Get unit files for user
6+
find:
7+
path: "{{ __systemd_linger_data['units_dir'] }}"
8+
register: __systemd_find
9+
10+
- name: Cancel linger if no files
11+
command: loginctl disable-linger {{ __systemd_linger_user | quote }}
12+
changed_when: true
13+
register: __systemd_cancel_linger
14+
when: __systemd_find.matched == 0
15+
args:
16+
removes: /var/lib/systemd/linger/{{ __systemd_linger_user }}
17+
18+
- name: Wait for user session to exit closing state # noqa no-handler
19+
command: loginctl show-user --value -p State {{ __systemd_linger_user | quote }}
20+
register: __systemd_user_state
21+
changed_when: false
22+
until: __systemd_user_state.stdout != "closing"
23+
when: __systemd_cancel_linger is changed
24+
ignore_errors: true
25+
26+
# see https://github.com/systemd/systemd/issues/26744#issuecomment-2261509208
27+
- name: Handle user stuck in closing state
28+
vars:
29+
__pat: "Failed to get user: User ID .* is not logged in or lingering"
30+
when:
31+
- __systemd_cancel_linger is changed
32+
- __systemd_user_state is failed
33+
- not __systemd_user_state.stderr is match(__pat)
34+
block:
35+
- name: Stop logind
36+
service:
37+
name: systemd-logind
38+
state: stopped
39+
40+
- name: Wait for user session to exit closing state
41+
command: loginctl show-user --value -p State {{ __systemd_linger_user | quote }}
42+
changed_when: false
43+
register: __systemd_user_state
44+
until: __systemd_user_state.stderr is match(__pat) or
45+
__systemd_user_state.stdout != "closing"
46+
failed_when:
47+
- not __systemd_user_state.stderr is match(__pat)
48+
- __systemd_user_state.stdout == "closing"
49+
50+
- name: Restart logind
51+
service:
52+
name: systemd-logind
53+
state: started

tasks/get_systemd_units_info.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-License-Identifier: MIT
2+
# Inputs:
3+
# __systemd_units_info
4+
# Outputs:
5+
# ansible_facts
6+
---
7+
- name: Gather systemd units state for system and user units
8+
systemd_units:
9+
user: "{{ __systemd_units_info.key }}"
10+
user_facts: "{{ ansible_facts['systemd_units_user'] | d({}) }}"
11+
become: "{{ __systemd_units_info.key != 'root' }}"
12+
become_user: "{{ __systemd_units_info.key if __systemd_units_info.key != 'root'
13+
else omit }}"
14+
environment:
15+
XDG_RUNTIME_DIR: "{{ __systemd_units_info.value['xdg_dir'] }}"

tasks/get_update_user_info.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SPDX-License-Identifier: MIT
2+
# Inputs:
3+
# __systemd_user_name
4+
# Outputs:
5+
# __systemd_user_info updated with getent info from user
6+
---
7+
- name: Get user information
8+
getent:
9+
database: passwd
10+
key: "{{ __systemd_user_name }}"
11+
fail_key: true
12+
when: "'getent_passwd' not in ansible_facts or
13+
__systemd_user_name not in ansible_facts['getent_passwd']"
14+
15+
- name: Update systemd user info with new user info
16+
when: __systemd_user_name not in __systemd_user_info
17+
set_fact:
18+
__systemd_user_info: |
19+
{% set getent = ansible_facts["getent_passwd"][__systemd_user_name] %}
20+
{% set rv = __systemd_user_info | d({}) %}
21+
{% if __systemd_user_name not in rv %}
22+
{% set _ = rv.__setitem__(__systemd_user_name, {}) %}
23+
{% endif %}
24+
{% if "xdg_dir" not in rv[__systemd_user_name] %}
25+
{% set xdg_dir = "/run/user/" ~ getent[1] %}
26+
{% set _ = rv[__systemd_user_name].update({"xdg_dir": xdg_dir}) %}
27+
{% endif %}
28+
{% if "units_dir" not in rv[__systemd_user_name] %}
29+
{% if __systemd_user_name == "root" %}
30+
{% set _ = rv[__systemd_user_name].update({"units_dir": __admin_units_dir}) %}
31+
{% else %}
32+
{% set units_dir = getent[4] ~ '/' ~ __user_units_dir %}
33+
{% set _ = rv[__systemd_user_name].update({"units_dir": units_dir}) %}
34+
{% endif %}
35+
{% endif %}
36+
{% if "group" not in rv[__systemd_user_name] %}
37+
{% set group = getent[2] %}
38+
{% set _ = rv[__systemd_user_name].update({"group": group}) %}
39+
{% endif %}
40+
{{ rv }}

0 commit comments

Comments
 (0)