Skip to content

Commit f81dc49

Browse files
committed
feat: Improve dotfiles bootstrap script
- Remove broken spinner animation that was eating terminal output - Replace version box with ASCII art banner for TechDufus Dotfiles - Add comprehensive Ctrl+C interrupt handler with cleanup - Add sudo detection system for graceful privilege degradation - Update roles to handle non-sudo scenarios with fallback strategies - Add documentation for sudo fallback patterns The dotfiles script now provides better visual feedback without animation issues and handles interruptions gracefully. The sudo detection allows the playbook to run with limited privileges when necessary.
1 parent d1c07a1 commit f81dc49

File tree

12 files changed

+1023
-170
lines changed

12 files changed

+1023
-170
lines changed

bin/dotfiles

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,26 @@ detect_os() {
209209
fi
210210
}
211211

212+
function print_banner() {
213+
echo ""
214+
echo -e "${LGREEN} ████████╗███████╗ ██████╗██╗ ██╗██████╗ ██╗ ██╗███████╗██╗ ██╗███████╗${NC}"
215+
echo -e "${LGREEN} ╚══██╔══╝██╔════╝██╔════╝██║ ██║██╔══██╗██║ ██║██╔════╝██║ ██║██╔════╝${NC}"
216+
echo -e "${LGREEN} ██║ █████╗ ██║ ███████║██║ ██║██║ ██║█████╗ ██║ ██║███████╗${NC}"
217+
echo -e "${LGREEN} ██║ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ██║ ██║╚════██║${NC}"
218+
echo -e "${LGREEN} ██║ ███████╗╚██████╗██║ ██║██████╔╝╚██████╔╝██║ ╚██████╔╝███████║${NC}"
219+
echo -e "${LGREEN} ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝${NC}"
220+
echo ""
221+
echo -e "${LGREEN} ██████╗ ██████╗ ████████╗███████╗██╗██╗ ███████╗███████╗${NC}"
222+
echo -e "${LGREEN} ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║██║ ██╔════╝██╔════╝${NC}"
223+
echo -e "${LGREEN} ██║ ██║██║ ██║ ██║ █████╗ ██║██║ █████╗ ███████╗${NC}"
224+
echo -e "${LGREEN} ██║ ██║██║ ██║ ██║ ██╔══╝ ██║██║ ██╔══╝ ╚════██║${NC}"
225+
echo -e "${LGREEN} ██████╔╝╚██████╔╝ ██║ ██║ ██║███████╗███████╗███████║${NC}"
226+
echo -e "${LGREEN} ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝${NC}"
227+
echo ""
228+
}
229+
230+
print_banner
231+
212232
dotfiles_os=$(detect_os)
213233
__task "Loading Setup for detected OS: $dotfiles_os"
214234
case $dotfiles_os in

main.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
tags:
3333
- always
3434

35+
- name: Detect Sudo
36+
ansible.builtin.import_tasks: pre_tasks/detect_sudo.yml
37+
tags:
38+
- always
39+
3540
- name: Detect 1Password
3641
ansible.builtin.import_tasks: pre_tasks/detect_1password.yml
3742
tags:

pre_tasks/detect_sudo.yml

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
# Comprehensive sudo detection for cross-platform compatibility
3+
# Sets facts that can be used throughout the playbook to handle privilege escalation
4+
5+
- name: Initialize sudo detection variables
6+
set_fact:
7+
has_sudo: false
8+
sudo_method: 'none'
9+
sudo_requires_password: true
10+
can_install_packages: false
11+
sudo_test_command: 'echo "sudo test successful"'
12+
detected_package_manager: ''
13+
privilege_escalation_available: false
14+
15+
- name: Detect package manager
16+
block:
17+
- name: Check for brew (macOS)
18+
command: which brew
19+
register: brew_check
20+
ignore_errors: true
21+
changed_when: false
22+
no_log: true
23+
24+
- name: Check for apt-get (Debian/Ubuntu)
25+
command: which apt-get
26+
register: apt_check
27+
ignore_errors: true
28+
changed_when: false
29+
no_log: true
30+
31+
- name: Check for pacman (Arch)
32+
command: which pacman
33+
register: pacman_check
34+
ignore_errors: true
35+
changed_when: false
36+
no_log: true
37+
38+
- name: Set detected package manager
39+
set_fact:
40+
detected_package_manager: >-
41+
{%- if brew_check.rc == 0 -%}brew
42+
{%- elif apt_check.rc == 0 -%}apt
43+
{%- elif pacman_check.rc == 0 -%}pacman
44+
{%- else -%}none{%- endif -%}
45+
46+
- name: Test passwordless sudo
47+
block:
48+
- name: Test sudo without password
49+
command: sudo -n {{ sudo_test_command }}
50+
register: sudo_nopass_test
51+
ignore_errors: true
52+
changed_when: false
53+
failed_when: false
54+
no_log: true
55+
56+
- name: Set passwordless sudo facts
57+
set_fact:
58+
has_sudo: true
59+
sudo_method: 'sudo'
60+
sudo_requires_password: false
61+
privilege_escalation_available: true
62+
when: sudo_nopass_test.rc == 0
63+
64+
- name: Test sudo with cached credentials
65+
when: not has_sudo
66+
block:
67+
- name: Check if sudo credentials are cached
68+
command: sudo -v
69+
register: sudo_cached_test
70+
ignore_errors: true
71+
changed_when: false
72+
failed_when: false
73+
no_log: true
74+
75+
- name: Test sudo with potentially cached password
76+
command: sudo {{ sudo_test_command }}
77+
register: sudo_cached_exec
78+
ignore_errors: true
79+
changed_when: false
80+
failed_when: false
81+
when: sudo_cached_test.rc == 0
82+
no_log: true
83+
84+
- name: Set sudo with cached credentials facts
85+
set_fact:
86+
has_sudo: true
87+
sudo_method: 'sudo'
88+
sudo_requires_password: true
89+
privilege_escalation_available: true
90+
when:
91+
- sudo_cached_test.rc == 0
92+
- sudo_cached_exec.rc == 0
93+
94+
- name: Test alternative privilege escalation methods
95+
when: not has_sudo
96+
block:
97+
- name: Test doas (OpenBSD/some Linux)
98+
command: doas {{ sudo_test_command }}
99+
register: doas_test
100+
ignore_errors: true
101+
changed_when: false
102+
failed_when: false
103+
no_log: true
104+
105+
- name: Set doas facts
106+
set_fact:
107+
has_sudo: true
108+
sudo_method: 'doas'
109+
sudo_requires_password: "{{ 'password' in doas_test.stderr | default('') }}"
110+
privilege_escalation_available: true
111+
when: doas_test.rc == 0
112+
113+
- name: Test pkexec (PolicyKit)
114+
command: pkexec --disable-internal-agent {{ sudo_test_command }}
115+
register: pkexec_test
116+
ignore_errors: true
117+
changed_when: false
118+
failed_when: false
119+
when: not has_sudo
120+
no_log: true
121+
122+
- name: Set pkexec facts
123+
set_fact:
124+
has_sudo: true
125+
sudo_method: 'pkexec'
126+
sudo_requires_password: true
127+
privilege_escalation_available: true
128+
when:
129+
- not has_sudo
130+
- pkexec_test.rc == 0
131+
132+
- name: Determine package installation capability
133+
set_fact:
134+
can_install_packages: >-
135+
{{ (detected_package_manager == 'brew') or
136+
(has_sudo and detected_package_manager in ['apt', 'pacman']) }}
137+
138+
- name: Display privilege escalation detection results
139+
debug:
140+
msg:
141+
- "=== Privilege Escalation Detection Results ==="
142+
- "Has sudo/privilege access: {{ has_sudo }}"
143+
- "Method available: {{ sudo_method }}"
144+
- "Requires password: {{ sudo_requires_password }}"
145+
- "Package manager: {{ detected_package_manager }}"
146+
- "Can install packages: {{ can_install_packages }}"
147+
- "============================================="
148+
149+
- name: Set Ansible become method based on detection
150+
set_fact:
151+
ansible_become_method: >-
152+
{%- if sudo_method == 'doas' -%}doas
153+
{%- elif sudo_method == 'pkexec' -%}pkexec
154+
{%- elif sudo_method == 'sudo' -%}sudo
155+
{%- else -%}sudo{%- endif -%}
156+
when: has_sudo
157+
158+
- name: Note about Homebrew and sudo
159+
debug:
160+
msg: "Note: Homebrew on macOS does not require or accept sudo. Package installation is possible without privilege escalation."
161+
when:
162+
- detected_package_manager == 'brew'
163+
- ansible_distribution == 'MacOSX'
164+
165+
- name: Warning when no privilege escalation available
166+
debug:
167+
msg:
168+
- "⚠️ WARNING: No privilege escalation method detected!"
169+
- "The playbook will continue but some tasks may be skipped."
170+
- "Package installation and system modifications will not be possible."
171+
when: not has_sudo

roles/README_SUDO.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Sudo Detection Usage Guide
2+
3+
This guide explains how to use the sudo detection system in your Ansible roles.
4+
5+
## Available Facts
6+
7+
After the sudo detection pre-task runs, the following facts are available:
8+
9+
- `has_sudo`: Boolean indicating if any sudo/privilege escalation method works
10+
- `sudo_method`: The working method (`'sudo'`, `'doas'`, `'pkexec'`, or `'none'`)
11+
- `sudo_requires_password`: Boolean indicating if password is required
12+
- `can_install_packages`: Boolean indicating if package installation is possible
13+
- `detected_package_manager`: String with the package manager (`'brew'`, `'apt'`, `'pacman'`, or `'none'`)
14+
- `privilege_escalation_available`: Boolean for overall privilege status
15+
16+
## Usage Examples
17+
18+
### 1. Skip Tasks Without Sudo
19+
20+
```yaml
21+
- name: Install system packages
22+
ansible.builtin.apt:
23+
name: package-name
24+
state: present
25+
become: true
26+
when: can_install_packages | default(false)
27+
```
28+
29+
### 2. Provide Alternative for Non-Sudo Users
30+
31+
```yaml
32+
- name: Install to system location (with sudo)
33+
ansible.builtin.copy:
34+
src: myfile
35+
dest: /usr/local/bin/myfile
36+
mode: '0755'
37+
become: true
38+
when: has_sudo | default(false)
39+
40+
- name: Install to user location (without sudo)
41+
ansible.builtin.copy:
42+
src: myfile
43+
dest: "{{ ansible_env.HOME }}/.local/bin/myfile"
44+
mode: '0755'
45+
when: not (has_sudo | default(false))
46+
```
47+
48+
### 3. Warn When Skipping
49+
50+
```yaml
51+
- name: Install required packages
52+
ansible.builtin.apt:
53+
name:
54+
- package1
55+
- package2
56+
state: present
57+
become: true
58+
when: can_install_packages | default(false)
59+
register: install_result
60+
failed_when: false
61+
62+
- name: Warn if packages not installed
63+
ansible.builtin.debug:
64+
msg: "⚠️ Skipping package installation - sudo access not available"
65+
when: not (can_install_packages | default(false))
66+
```
67+
68+
### 4. Cross-Platform Package Installation
69+
70+
```yaml
71+
- name: Install package (macOS)
72+
community.general.homebrew:
73+
name: package-name
74+
state: present
75+
when:
76+
- ansible_distribution == "MacOSX"
77+
- detected_package_manager == "brew"
78+
79+
- name: Install package (Ubuntu/Debian)
80+
ansible.builtin.apt:
81+
name: package-name
82+
state: present
83+
become: true
84+
when:
85+
- ansible_distribution in ["Ubuntu", "Debian"]
86+
- can_install_packages | default(false)
87+
88+
- name: Install package (Arch)
89+
community.general.pacman:
90+
name: package-name
91+
state: present
92+
become: true
93+
when:
94+
- ansible_distribution == "Archlinux"
95+
- can_install_packages | default(false)
96+
```
97+
98+
### 5. Role-Level Conditions
99+
100+
Each role should handle its own sudo requirements internally. Roles that absolutely require sudo (like Docker) should check for `has_sudo` at the beginning and skip gracefully with clear messages if it's not available.
101+
102+
For roles that can partially function without sudo, add conditions to individual tasks that require elevated privileges.
103+
104+
## Best Practices
105+
106+
1. **Always use `| default(false)`** with boolean facts to handle undefined variables
107+
2. **Provide user-friendly warnings** when skipping important tasks
108+
3. **Consider alternative locations** for file installations (e.g., `~/.local/bin` instead of `/usr/local/bin`)
109+
4. **Test your roles** with `--check` mode to ensure they handle missing sudo gracefully
110+
5. **Use `failed_when: false`** or `ignore_errors: true` for tasks that might fail without sudo
111+
112+
## Testing
113+
114+
Test your role with different sudo scenarios:
115+
116+
```bash
117+
# Normal run
118+
dotfiles -t your-role
119+
120+
# Check mode
121+
dotfiles -t your-role --check
122+
123+
# Verbose to see skip messages
124+
dotfiles -t your-role -vvv
125+
```

0 commit comments

Comments
 (0)