Skip to content

Commit 273e9bb

Browse files
pablomhclaude
andcommitted
Convert Podman Quadlet deployment from rootful to rootless
Converts Foreman deployment to rootless Podman containers with dedicated service user and proper namespace isolation. Key changes: - Auto-allocate matching UID/GID for foreman service user - Map container volumes to proper UIDs (PostgreSQL:26, Redis:1001, Pulp:700) - Move certificates from /root to /var/lib/foreman with correct ownership - Add migration playbook for converting existing rootful deployments - Move Quadlet files to user scope (~/.config/containers/systemd) - Enable loginctl linger and configure unprivileged ports New components: - rootless_user role: Service user creation with auto-allocation - migrate-to-rootless playbook: Automated rootful-to-rootless migration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 52978f3 commit 273e9bb

File tree

30 files changed

+1691
-857
lines changed

30 files changed

+1691
-857
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
---
2+
# Migration Playbook: Rootful to Rootless Podman Deployment
3+
#
4+
# This playbook migrates an existing rootful Foreman Quadlet deployment to rootless.
5+
#
6+
# WARNING: This is a destructive operation that will:
7+
# - Stop all running services
8+
# - Transfer ownership of data volumes
9+
# - Remove system-scoped systemd units
10+
# - Recreate everything in user scope
11+
#
12+
# PREREQUISITES:
13+
# - Backup all data before running this migration
14+
# - Ensure no active users or operations are running
15+
# - Test this in a non-production environment first
16+
#
17+
# USAGE:
18+
# ansible-playbook -i inventory migrate.yaml
19+
#
20+
- name: Migrate Foreman from rootful to rootless deployment
21+
hosts:
22+
- quadlet
23+
become: true
24+
vars_files:
25+
- "../../vars/defaults.yml"
26+
- "../../vars/base.yaml"
27+
- "../../roles/postgresql/defaults/main.yml"
28+
- "../../roles/redis/defaults/main.yml"
29+
- "../../roles/pulp/defaults/main.yaml"
30+
vars:
31+
migration_backup_dir: "/var/backups/foreman-migration-{{ ansible_date_time.iso8601_basic_short }}"
32+
# Storage directories with their container-specific UIDs/GIDs
33+
# UIDs are from the container images, not the host
34+
migration_data_volumes:
35+
- path: "{{ postgresql_data_dir }}"
36+
uid: "{{ postgresql_container_uid }}"
37+
gid: "{{ postgresql_container_gid }}"
38+
- path: "{{ redis_data_dir }}"
39+
uid: "{{ redis_container_uid }}"
40+
gid: "{{ redis_container_gid }}"
41+
- path: /var/lib/pulp
42+
uid: "{{ pulp_container_uid }}"
43+
gid: "{{ pulp_container_gid }}"
44+
# Legacy variable for backwards compatibility
45+
migration_data_paths: "{{ migration_data_volumes | map(attribute='path') | list }}"
46+
47+
tasks:
48+
- name: Verify this is a rootful deployment
49+
ansible.builtin.stat:
50+
path: /etc/containers/systemd/foreman.container
51+
register: migration_rootful_check
52+
failed_when: not migration_rootful_check.stat.exists
53+
54+
- name: Display migration warning
55+
ansible.builtin.pause:
56+
prompt: |
57+
58+
================================================================
59+
WARNING: DESTRUCTIVE MIGRATION IN PROGRESS
60+
================================================================
61+
62+
This will migrate your Foreman deployment from rootful to
63+
rootless Podman containers.
64+
65+
ALL SERVICES WILL BE STOPPED during migration.
66+
67+
Backup directory: {{ migration_backup_dir }}
68+
69+
Press Ctrl+C to abort, or Enter to continue...
70+
================================================================
71+
72+
- name: Create backup directory
73+
ansible.builtin.file:
74+
path: "{{ migration_backup_dir }}"
75+
state: directory
76+
mode: '0700'
77+
78+
# ============================================================
79+
# Phase 1: Stop and backup rootful deployment
80+
# ============================================================
81+
82+
- name: Stop foreman.target (rootful)
83+
ansible.builtin.systemd:
84+
name: foreman.target
85+
state: stopped
86+
failed_when: false
87+
register: migration_stop_target
88+
89+
- name: Stop all Foreman-related services (rootful)
90+
ansible.builtin.systemd:
91+
name: "{{ item }}"
92+
state: stopped
93+
loop:
94+
- foreman
95+
- candlepin
96+
- pulp-api
97+
- pulp-content
98+
- pulp-worker.target
99+
- postgresql
100+
- redis
101+
- foreman-proxy
102+
failed_when: false
103+
104+
- name: Backup rootful quadlet files
105+
ansible.builtin.copy:
106+
src: /etc/containers/systemd/
107+
dest: "{{ migration_backup_dir }}/quadlets/"
108+
remote_src: true
109+
mode: '0600'
110+
111+
- name: Backup rootful systemd units
112+
ansible.builtin.shell: |
113+
mkdir -p {{ migration_backup_dir }}/systemd/
114+
cp -a /etc/systemd/system/foreman* {{ migration_backup_dir }}/systemd/ 2>/dev/null || true
115+
cp -a /etc/systemd/system/pulp* {{ migration_backup_dir }}/systemd/ 2>/dev/null || true
116+
cp -a /etc/systemd/system/dynflow* {{ migration_backup_dir }}/systemd/ 2>/dev/null || true
117+
args:
118+
executable: /bin/bash
119+
changed_when: true
120+
121+
- name: List rootful Podman secrets
122+
ansible.builtin.command: podman secret ls --format json
123+
register: migration_rootful_secrets
124+
changed_when: false
125+
126+
- name: Save secret list to backup
127+
ansible.builtin.copy:
128+
content: "{{ migration_rootful_secrets.stdout }}"
129+
dest: "{{ migration_backup_dir }}/secrets.json"
130+
mode: '0600'
131+
132+
# ============================================================
133+
# Phase 2: Create rootless user and setup
134+
# ============================================================
135+
136+
- name: Setup rootless user environment (creates user/group with auto-allocated matching UID/GID)
137+
ansible.builtin.include_role:
138+
name: rootless_user
139+
140+
# ============================================================
141+
# Phase 3: Migrate data volumes
142+
# ============================================================
143+
144+
- name: Get current ownership of data directories
145+
ansible.builtin.stat:
146+
path: "{{ item }}"
147+
loop: "{{ migration_data_paths }}"
148+
register: migration_data_stat
149+
failed_when: false
150+
151+
- name: Display volume migration plan
152+
ansible.builtin.debug:
153+
msg: |
154+
Migrating volumes with podman unshare (mapping to container UIDs):
155+
{% for volume in migration_data_volumes %}
156+
- {{ volume.path }}: will be owned by container UID {{ volume.uid }}:{{ volume.gid }}
157+
{% endfor %}
158+
159+
- name: Change ownership of data directories using podman unshare
160+
ansible.builtin.shell: |
161+
cd /tmp
162+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} \
163+
podman unshare chown -R {{ item.uid }}:{{ item.gid }} {{ item.path }}
164+
args:
165+
executable: /bin/bash
166+
loop: "{{ migration_data_volumes }}"
167+
when: migration_data_stat.results | selectattr('stat.exists') | list | length > 0
168+
changed_when: true
169+
170+
- name: Update directory ownership to foreman user
171+
ansible.builtin.file:
172+
path: "{{ item }}"
173+
owner: "{{ foreman_service_user }}"
174+
group: "{{ foreman_service_group }}"
175+
recurse: false
176+
state: directory
177+
loop: "{{ migration_data_paths }}"
178+
179+
# ============================================================
180+
# Phase 4: Remove rootful configuration
181+
# ============================================================
182+
183+
- name: Disable rootful services
184+
ansible.builtin.systemd:
185+
name: "{{ item }}"
186+
enabled: false
187+
loop:
188+
- foreman
189+
- candlepin
190+
- pulp-api
191+
- pulp-content
192+
- pulp-worker.target
193+
- postgresql
194+
- redis
195+
- foreman-proxy
196+
- foreman.target
197+
failed_when: false
198+
199+
- name: Remove rootful quadlet files
200+
ansible.builtin.file:
201+
path: "/etc/containers/systemd/{{ item }}"
202+
state: absent
203+
loop:
204+
- foreman.container
205+
- candlepin.container
206+
- pulp-api.container
207+
- pulp-content.container
208+
- pulp-worker@.container
209+
- postgresql.container
210+
- redis.container
211+
- foreman-proxy.container
212+
- dynflow-sidekiq@.container
213+
- foreman-recurring@*.container
214+
215+
- name: Remove rootful systemd units
216+
ansible.builtin.file:
217+
path: "/etc/systemd/system/{{ item }}"
218+
state: absent
219+
loop:
220+
- foreman.target
221+
- pulp-worker.target
222+
- foreman-recurring@*.timer
223+
- foreman-recurring@*.service
224+
- dynflow-sidekiq@*.service
225+
226+
- name: Remove rootful Podman secrets
227+
ansible.builtin.shell: |
228+
for secret in $(podman secret ls --format '{{{{.Name}}}}'); do
229+
podman secret rm "$secret" 2>/dev/null || true
230+
done
231+
args:
232+
executable: /bin/bash
233+
changed_when: true
234+
235+
- name: Reload systemd daemon (system scope)
236+
ansible.builtin.systemd:
237+
daemon_reload: true
238+
239+
# ============================================================
240+
# Phase 5: Deploy rootless configuration
241+
# ============================================================
242+
243+
- name: Run rootless deployment
244+
ansible.builtin.include_role:
245+
name: "{{ item }}"
246+
loop:
247+
- certificates
248+
- postgresql
249+
- redis
250+
- candlepin
251+
- pulp
252+
- foreman
253+
- systemd_target
254+
- foreman_proxy
255+
256+
# ============================================================
257+
# Phase 6: Verification
258+
# ============================================================
259+
260+
- name: Wait for services to stabilize
261+
ansible.builtin.pause:
262+
seconds: 10
263+
264+
- name: Verify rootless services are running
265+
ansible.builtin.command: |
266+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} \
267+
systemctl --user is-active {{ item }}
268+
loop:
269+
- foreman
270+
- postgresql
271+
- redis
272+
- candlepin
273+
- pulp-api
274+
- pulp-content
275+
register: migration_service_check
276+
changed_when: false
277+
failed_when: migration_service_check.stdout != "active"
278+
279+
- name: Display migration summary
280+
ansible.builtin.debug:
281+
msg: |
282+
283+
================================================================
284+
MIGRATION COMPLETED SUCCESSFULLY
285+
================================================================
286+
287+
Foreman is now running in rootless mode.
288+
289+
Service user: {{ foreman_service_user }} (UID {{ foreman_service_uid }})
290+
Quadlets: {{ foreman_quadlet_dir }}
291+
Systemd units: {{ foreman_systemd_user_dir }}
292+
293+
Backup directory: {{ migration_backup_dir }}
294+
295+
Verify services:
296+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} systemctl --user status foreman.target
297+
298+
View logs:
299+
sudo journalctl --user -u foreman -f
300+
301+
================================================================
302+
303+
- name: Save migration report
304+
ansible.builtin.copy:
305+
content: |
306+
Foreman Rootful to Rootless Migration Report
307+
=============================================
308+
309+
Migration Date: {{ ansible_date_time.iso8601 }}
310+
Hostname: {{ ansible_facts['fqdn'] }}
311+
312+
Service User: {{ foreman_service_user }} (UID {{ foreman_service_uid }})
313+
Service Group: {{ foreman_service_group }} (GID {{ foreman_service_gid }})
314+
315+
Quadlet Directory: {{ foreman_quadlet_dir }}
316+
Systemd User Directory: {{ foreman_systemd_user_dir }}
317+
XDG_RUNTIME_DIR: {{ foreman_xdg_runtime_dir }}
318+
319+
Migrated Data Volumes:
320+
{% for path in migration_data_paths %}
321+
- {{ path }}
322+
{% endfor %}
323+
324+
Backup Location: {{ migration_backup_dir }}
325+
326+
Active Services:
327+
{% for item in migration_service_check.results %}
328+
- {{ item.item }}: {{ item.stdout }}
329+
{% endfor %}
330+
331+
Verification Commands:
332+
----------------------
333+
334+
# Check service status
335+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} systemctl --user status foreman.target
336+
337+
# List containers
338+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} podman ps
339+
340+
# View logs
341+
sudo journalctl --user -u foreman -f
342+
343+
# Check linger status
344+
loginctl show-user {{ foreman_service_user }}
345+
346+
Rollback Instructions:
347+
----------------------
348+
349+
If you need to rollback to rootful deployment:
350+
351+
1. Stop rootless services:
352+
sudo -u {{ foreman_service_user }} XDG_RUNTIME_DIR={{ foreman_xdg_runtime_dir }} systemctl --user stop foreman.target
353+
354+
2. Restore rootful quadlets:
355+
sudo cp -a {{ migration_backup_dir }}/quadlets/* /etc/containers/systemd/
356+
sudo cp -a {{ migration_backup_dir }}/systemd/* /etc/systemd/system/
357+
358+
3. Reload and start:
359+
sudo systemctl daemon-reload
360+
sudo systemctl start foreman.target
361+
362+
IMPORTANT: Keep the backup directory until you've verified the migration is stable.
363+
dest: "{{ migration_backup_dir }}/MIGRATION_REPORT.txt"
364+
mode: '0600'
365+
...

src/roles/candlepin/defaults/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ candlepin_ciphers:
1515
candlepin_container_image: quay.io/foreman/candlepin
1616
candlepin_container_tag: "4.4.14"
1717

18+
candlepin_keystore_path: /var/lib/foreman/candlepin.keystore
19+
candlepin_truststore_path: /var/lib/foreman/candlepin.truststore
20+
1821
candlepin_database_host: localhost
1922
candlepin_database_port: 5432
2023
candlepin_database_ssl: false

src/roles/candlepin/handlers/main.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
ansible.builtin.systemd:
44
name: candlepin
55
state: restarted
6+
scope: user
7+
environment:
8+
XDG_RUNTIME_DIR: "{{ foreman_xdg_runtime_dir }}"
9+
become: true
10+
become_user: "{{ foreman_service_user }}"

0 commit comments

Comments
 (0)