-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from kysrpex/remap_user_enhancement
Enhance remap user tasks from os_setup role
- Loading branch information
Showing
2 changed files
with
269 additions
and
51 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
#!/usr/bin/env python | ||
"""Script used by the `remap_user.yml` task to remap users and groups. | ||
Given a list of existing user or group ids and desired user or group ids, | ||
compute a mapping from existing user or group ids to new user or group ids that | ||
do not conflict with other existing ids nor the desired ids. | ||
For example, assume service "fwupd" has created a user named "fwupd" and the | ||
operating system has assigned UID 999 to it. Furthermore, assume that two new | ||
applications need to run on the server (for example, Galaxy and HTCondor) with | ||
UIDs 999 and 3 respectively (for example, because they need to access a remote | ||
filesystem using NFS and the server has already allocated UIDs 999 and 3 to the | ||
applications, meaning that existing files belonging to the applications are | ||
already owned by UIDs 999 and 3 respectively). In addition, assume the | ||
/etc/passwd file looks as follows: | ||
``` | ||
root:x:0:0::/root:/bin/bash | ||
bin:x:1:1::/:/usr/bin/nologin | ||
daemon:x:2:2::/:/usr/bin/nologin | ||
mail:x:8:12::/var/spool/mail:/usr/bin/nologin | ||
fwupd:x:999:962:Firmware update daemon:/var/lib/fwupd:/usr/bin/nologin | ||
``` | ||
It is then necessary to remap the "fwupd" user to a different UID so that 999 | ||
becomes free for the first application (e.g. Galaxy). This script can find a | ||
new UID for "fwupd" that is different both from 999 and 3. To do so, call it | ||
passing as arguments the existing and desired UIDs as demonstrated below. | ||
``` | ||
./remap_user.py \ | ||
-e 0:root \ | ||
-e 1:bin \ | ||
-e 2:daemon \ | ||
-e 8:mail \ | ||
-d 999:galaxy \ | ||
-d 3:condor | ||
``` | ||
The output of the call is shown below. Notice that user 999 ("fwupd") does not | ||
get mapped to UID 3, despite it being free, since it is meant to belong to user | ||
"condor". | ||
``` | ||
EXISTING TARGET | ||
999 4 | ||
``` | ||
""" | ||
|
||
from argparse import ArgumentParser | ||
from os.path import basename, splitext | ||
from sys import stdout | ||
from typing import Iterator, Mapping | ||
|
||
|
||
def ordered_complement(integers: set[int]) -> Iterator[int]: | ||
"""Yield from the complement of a set of nonnegative integers. | ||
Yield from the complement of a set of nonnegative integers in ascending | ||
order. | ||
Args: | ||
integers: Set of nonnegative integers to yield the complement of. | ||
Yields: | ||
Nonnegative integers from the complement of the set passed as input, in | ||
ascending order. | ||
""" | ||
i = 0 | ||
while True: | ||
if i not in integers: | ||
yield i | ||
i += 1 | ||
|
||
|
||
def resolve_conflicts( | ||
mapping_existing: Mapping[int, str], mapping_desired: Mapping[int, str] | ||
) -> Mapping[int, int]: | ||
"""Resolve conflicts between existing and desired user or group ids. | ||
Given a mapping defining the existing group or user ids and desired user or | ||
group ids, compute a mapping from existing group or user ids to new ids | ||
that do not conflict with other existing ids nor the desired ids. | ||
Args: | ||
mapping_existing: Already defined user or group ids. | ||
mapping_desired: Desired arrangement of user or group ids | ||
(non-exhaustive). | ||
Returns: | ||
Mapping from existing user or group ids to new user or group ids that | ||
resolves the conflicts between existing and desired ids. | ||
""" | ||
conflicts = { | ||
id_ | ||
for id_ in mapping_existing.keys() & mapping_desired.keys() | ||
if mapping_existing[id_] != mapping_desired[id_] | ||
} | ||
resolution = { | ||
id_existing: id_target | ||
for id_existing, id_target in zip( | ||
conflicts, | ||
ordered_complement( | ||
mapping_existing.keys() | mapping_desired.keys() | ||
), | ||
) | ||
} | ||
return resolution | ||
|
||
|
||
def make_parser() -> ArgumentParser: | ||
"""Command line interface for this script.""" | ||
parser = ArgumentParser( | ||
prog=splitext(basename(__file__))[0], | ||
description="Remap user or group ids", | ||
) | ||
|
||
def convert_argument(string: str) -> tuple[int, str]: | ||
"""Convert a user or group id in the form "ID:NAME" to a tuple.""" | ||
id_, name = string.split(":") | ||
id_ = int(id_) | ||
return id_, name | ||
|
||
parser.add_argument( | ||
"-e", | ||
"--existing", | ||
dest="existing", | ||
metavar="ID:NAME", | ||
action="extend", | ||
default=[], | ||
nargs="+", | ||
required=True, | ||
type=convert_argument, | ||
help="existing user/group id and name (separated by a colon)", | ||
) | ||
parser.add_argument( | ||
"-d", | ||
"--desired", | ||
dest="desired", | ||
metavar="ID:NAME", | ||
action="extend", | ||
default=[], | ||
nargs="+", | ||
required=True, | ||
type=convert_argument, | ||
help="desired user/group id and name (separated by a colon)", | ||
) | ||
|
||
return parser | ||
|
||
|
||
if __name__ == "__main__": | ||
command_parser = make_parser() | ||
command_args = command_parser.parse_args() | ||
|
||
existing = {id_: name for id_, name in command_args.existing} | ||
desired = {id_: name for id_, name in command_args.desired} | ||
mapping = resolve_conflicts(existing, desired) | ||
|
||
col_length = max( | ||
len(str(x)) | ||
for x in ("EXISTING", "TARGET", *mapping.keys(), *mapping.values()) | ||
) | ||
row_fmt = "\t".join((f"{{:>{col_length}}}",) * 2) | ||
if mapping: | ||
print(row_fmt.format("EXISTING", "TARGET"), file=stdout) | ||
for existing_id, target_id in mapping.items(): | ||
print(row_fmt.format(existing_id, target_id), file=stdout) |
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 |
---|---|---|
@@ -1,74 +1,124 @@ | ||
# This role provides a collection of tasks "create_user.yml" that creates the | ||
# users and groups defined in `handy_users` and `handy_groups`. Said tasks | ||
# allow to choose the UID and GID of the users and groups to be created. | ||
# | ||
# However, the specified UIDs and GIDs may already have been taken by other | ||
# users or groups. The tasks in this file, "remap_user.yml", map those users | ||
# and groups to different UIDs and GIDs so that the ones specified in | ||
# `handy_users` and `handy_groups` become available to the new users. | ||
# | ||
# For example, in CentOS 8, UID 999 is allocated to user `systemd-coredump`, | ||
# but in usegalaxy.eu's infrastructure, UID 999 is meant to be assigned to | ||
# user `galaxy` (the Galaxy application). The tasks in this file can remap user | ||
# `systemd-coredump` to another UID so that 999 becomes free to the `galaxy` | ||
# user, that would be created afterward by the tasks in create_user.yml. | ||
# | ||
# A much more detailed example is available in the file | ||
# roles/os_setup/files/remap_user.py. | ||
--- | ||
# RHEL uses GID 999 and UID 999, both of which need to be | ||
# mapped to the galaxy user and group. | ||
|
||
- name: Get all groups | ||
getent: | ||
- name: Get all groups. | ||
ansible.builtin.getent: | ||
database: group | ||
split: ':' | ||
|
||
- name: Get all users | ||
getent: | ||
- name: Get all users. | ||
ansible.builtin.getent: | ||
database: passwd | ||
split: ':' | ||
|
||
- name: Remap GID 999 if Galaxy group is not present | ||
- name: Compute user mapping. | ||
become: false | ||
block: | ||
- name: Check for GID 999 group | ||
- name: Compute user mapping using script. | ||
ansible.builtin.shell: | ||
cmd: grep 999 /etc/group | ||
register: check_group | ||
chdir: "{{ role_path }}/files" | ||
cmd: | | ||
set -o pipefail; | ||
./remap_user.py \ | ||
{% for name, data in getent_passwd.items() %} -e {{ data[1] }}:{{ name }}{% endfor %} \ | ||
{% for user in handy_users %} -d {{ user.user_uid }}:{{ user.user_name }}{% endfor %} \ | ||
| tail -n +2 | ||
register: os_setup_user_mapping_script | ||
changed_when: false | ||
failed_when: check_group.rc not in (0, 1) | ||
delegate_to: localhost | ||
|
||
- name: Print return information from the previous task | ||
ansible.builtin.debug: | ||
var: check_group | ||
when: debug | ||
- name: Process script output. | ||
ansible.builtin.set_fact: | ||
os_setup_user_mapping: "{{ os_setup_user_mapping | default([]) + [item | split('\t') | map('trim') | list] }}" | ||
loop: "{{ os_setup_user_mapping_script.stdout_lines }}" | ||
# Each line of output is of the form below | ||
# " 998\t 7" | ||
# " 999\t 13" | ||
# and the final outcome is a list of lists. | ||
# [["998", "7"], ["999", "9"]] | ||
|
||
- name: Replace in group file | ||
ansible.builtin.replace: | ||
path: /etc/group | ||
regexp: '999' | ||
replace: "500" | ||
when: (not ansible_check_mode and check_group.rc == 0 and not check_group.stdout == '') | ||
|
||
- name: Search and replace 999 group files | ||
- name: Compute group mapping. | ||
become: false | ||
block: | ||
- name: Compute group mapping using script. | ||
ansible.builtin.shell: | ||
cmd: "find / -mount -gid 999 -exec chgrp 500 '{}' +" | ||
ignore_errors: true | ||
when: check_group.rc == 0 | ||
tags: | ||
- ignore_errors | ||
when: (not "galaxy" in getent_group.keys()) | ||
chdir: "{{ role_path }}/files" | ||
cmd: | | ||
set -o pipefail; | ||
./remap_user.py \ | ||
{% for name, data in getent_group.items() %} -e {{ data[1] }}:{{ name }}{% endfor %} \ | ||
{% for group in handy_groups %} -d {{ group.group_gid }}:{{ group.group_name }}{% endfor %} \ | ||
| tail -n +2 | ||
register: os_setup_group_mapping_script | ||
changed_when: false | ||
delegate_to: localhost | ||
|
||
- name: Process script output. | ||
ansible.builtin.set_fact: | ||
os_setup_group_mapping: "{{ os_setup_group_mapping | default([]) + [item | split('\t') | map('trim') | list] }}" | ||
loop: "{{ os_setup_group_mapping_script.stdout_lines }}" | ||
# Each line of output is of the form below | ||
# " 998\t 7" | ||
# " 999\t 13" | ||
# and the final outcome is a list of lists. | ||
# [["998", "7"], ["999", "9"]] | ||
|
||
- name: Remap UID 999 if Galaxy user is not present | ||
- name: Replace users. | ||
become: true | ||
when: os_setup_user_mapping is defined | ||
block: | ||
- name: Check for UID 999 in user file | ||
- name: Replace user in users file. | ||
ansible.builtin.replace: | ||
path: /etc/passwd | ||
regexp: "^((?:[^:]*:){2})({{ item.0 }})((?::[^:]*){4})$" | ||
replace: '\g<1>{{ item.1 }}\g<3>' | ||
validate: /usr/sbin/pwck --read-only %s | ||
loop: "{{ os_setup_user_mapping }}" | ||
|
||
- name: Search and replace owner of user's files. | ||
ansible.builtin.shell: | ||
cmd: grep 999 /etc/passwd | ||
register: check_user | ||
changed_when: false | ||
failed_when: check_user.rc not in (0, 1) | ||
cmd: "find / -mount -uid {{ item.0 }} -exec chown {{ item.1 }} '{}' +" | ||
loop: "{{ os_setup_user_mapping }}" | ||
changed_when: true | ||
|
||
- name: Print return information from the previous task | ||
ansible.builtin.debug: | ||
var: check_user | ||
when: debug | ||
- name: Replace groups. | ||
become: true | ||
when: os_setup_group_mapping is defined | ||
block: | ||
- name: Replace group in groups file. | ||
ansible.builtin.replace: | ||
path: /etc/group | ||
regexp: "^((?:[^:]*:){2})({{ item.0 }})((?::[^:]*){1})$" | ||
replace: '\g<1>{{ item.1 }}\g<3>' | ||
validate: /usr/sbin/grpck --read-only %s | ||
loop: "{{ os_setup_group_mapping }}" | ||
|
||
- name: Replace in passwd file | ||
- name: Replace group in users file. | ||
ansible.builtin.replace: | ||
path: /etc/passwd | ||
regexp: '999' | ||
replace: "500" | ||
validate: /usr/sbin/pwck %s | ||
when: (not ansible_check_mode and check_user.rc == 0 and not check_user.stdout == '') | ||
regexp: "^((?:[^:]*:){3})({{ item.0 }})((?::[^:]*){3})$" | ||
replace: '\g<1>{{ item.1 }}\g<3>' | ||
validate: /usr/sbin/pwck --read-only %s | ||
loop: "{{ os_setup_group_mapping }}" | ||
|
||
- name: Search and replace 999 user files | ||
- name: Search and replace group of group's files. | ||
ansible.builtin.shell: | ||
cmd: "find / -mount -uid 999 -exec chown 500 '{}' +" | ||
ignore_errors: true | ||
when: check_user.rc == 0 | ||
tags: | ||
- ignore_errors | ||
when: (not "galaxy" in getent_passwd.keys()) | ||
cmd: "find / -mount -gid {{ item.0 }} -exec chgrp {{ item.1 }} '{}' +" | ||
loop: "{{ os_setup_group_mapping }}" | ||
changed_when: true |