Repository navigation
Expand file tree
/
Copy pathbootstrap
More file actions
executable file
·255 lines (225 loc) · 6.04 KB
/
Copy pathbootstrap
File metadata and controls
executable file
·255 lines (225 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./bootstrap <profile> [options] [-- extra ansible-playbook args]
Profiles:
headless Linux base development environment
gui Linux GUI workstation with Qtile
mac macOS base development environment
mac-gui macOS GUI workstation with Ghostty + AeroSpace
Options:
-i, --inventory PATH Inventory file or host list
-u, --user USER SSH user for remote Linux hosts
--host HOST Single host shortcut; expands to "HOST,"
--check Run Ansible check mode
--diff Show Ansible diffs
--tags TAGS Run only selected tags
--skip-tags TAGS Skip selected tags
--enable-tailscale Install/configure Tailscale for this run
--disable-tailscale
Disable Tailscale for this run
--extra-var KEY=VALUE
Pass an extra Ansible variable
--no-become-pass Do not prompt for sudo password on Linux profiles
If no Linux inventory target is provided, bootstrap uses gum to select a host or group from inventory/hosts.yml.
-h, --help Show this help
Examples:
./bootstrap headless --host linux-server --user stonecharioteer
./bootstrap gui -i inventory/gui.example.yml --check --diff
./bootstrap mac
./bootstrap mac-gui --tags gui
EOF
}
profile="${1:-}"
if [[ -z "${profile}" || "${profile}" == "-h" || "${profile}" == "--help" ]]; then
usage
exit 0
fi
shift
inventory=""
ssh_user=""
ask_become_pass=true
ansible_args=()
has_limit_arg() {
local previous=""
if (( ${#ansible_args[@]} == 0 )); then
return 1
fi
for arg in "${ansible_args[@]}"; do
if [[ "${previous}" == "--limit" || "${previous}" == "-l" ]]; then
return 0
fi
if [[ "${arg}" == --limit=* || "${arg}" == -l=* || "${arg}" == "--limit" || "${arg}" == "-l" ]]; then
return 0
fi
previous="${arg}"
done
return 1
}
select_inventory_target() {
local profile_group="$1"
local default_inventory="inventory/hosts.yml"
inventory="${inventory:-${default_inventory}}"
if ! command -v gum >/dev/null 2>&1; then
cat >&2 <<EOF
No inventory target was provided and gum is not installed.
Install gum or pass an explicit target, for example:
./bootstrap ${profile} -i ${inventory} -- --limit ${profile_group}
./bootstrap ${profile} --host linux-server
EOF
exit 1
fi
local selection
selection="$(ansible-inventory -i "${inventory}" --list | PROFILE_GROUP="${profile_group}" python3 -c '
import json
import os
import sys
profile_group = os.environ["PROFILE_GROUP"]
data = json.load(sys.stdin)
choices = []
def add_group(name):
if name in data and (data[name].get("hosts") or data[name].get("children")):
choices.append(f"group:{name}")
def collect_hosts(group_name, seen_groups=None, seen_hosts=None):
seen_groups = seen_groups or set()
seen_hosts = seen_hosts or []
if group_name in seen_groups or group_name not in data:
return seen_hosts
seen_groups.add(group_name)
for host in data[group_name].get("hosts", []):
if host not in seen_hosts:
seen_hosts.append(host)
for child in data[group_name].get("children", []):
collect_hosts(child, seen_groups, seen_hosts)
return seen_hosts
add_group(profile_group)
if profile_group == "headless_machines":
add_group("thinkpads")
elif profile_group == "gui_machines":
add_group("workstations")
for host in collect_hosts(profile_group):
choices.append(host)
for choice in choices:
print(choice)
' | gum choose --header "Select ${profile} target from ${inventory}")"
if [[ -z "${selection}" ]]; then
echo "No inventory target selected." >&2
exit 1
fi
if [[ "${selection}" == group:* ]]; then
selection="${selection#group:}"
fi
ansible_args+=(--limit "${selection}")
}
while [[ $# -gt 0 ]]; do
case "$1" in
-i|--inventory)
inventory="$2"
shift 2
;;
-u|--user)
ssh_user="$2"
shift 2
;;
--host)
inventory="$2,"
shift 2
;;
--check|--diff)
ansible_args+=("$1")
shift
;;
--tags|--skip-tags)
ansible_args+=("$1" "$2")
shift 2
;;
--enable-tailscale)
ansible_args+=(-e enable_tailscale=true)
shift
;;
--disable-tailscale)
ansible_args+=(-e enable_tailscale=false)
shift
;;
--extra-var)
ansible_args+=(-e "$2")
shift 2
;;
--no-become-pass)
ask_become_pass=false
shift
;;
--)
shift
ansible_args+=("$@")
break
;;
-h|--help)
usage
exit 0
;;
*)
ansible_args+=("$1")
shift
;;
esac
done
case "${profile}" in
headless)
playbook="playbooks/base-environment.yml"
needs_become=true
;;
gui)
playbook="playbooks/gui-environment.yml"
needs_become=true
;;
mac)
playbook="playbooks/macos-base-environment.yml"
needs_become=false
inventory="${inventory:-inventory/localhost.yml}"
;;
mac-gui)
playbook="playbooks/macos-gui-environment.yml"
needs_become=false
inventory="${inventory:-inventory/localhost.yml}"
;;
*)
echo "Unknown profile: ${profile}" >&2
usage >&2
exit 2
;;
esac
if [[ "${inventory}" != *, ]]; then
if [[ "${profile}" == "headless" ]] && ! has_limit_arg; then
select_inventory_target headless_machines
elif [[ "${profile}" == "gui" ]] && ! has_limit_arg; then
select_inventory_target gui_machines
fi
fi
command=(ansible-playbook)
if [[ -n "${inventory}" ]]; then
command+=(-i "${inventory}")
fi
if [[ -n "${ssh_user}" ]]; then
command+=(-u "${ssh_user}")
fi
if [[ "${needs_become}" == true && "${ask_become_pass}" == true ]]; then
command+=(--ask-become-pass)
fi
command+=("${playbook}")
if (( ${#ansible_args[@]} > 0 )); then
command+=("${ansible_args[@]}")
fi
cat <<EOF
Profile: ${profile}
Playbook: ${playbook}
Command: ${command[*]}
EOF
read -r -p "Continue? [y/N] " answer
case "${answer}" in
y|Y|yes|YES) ;;
*) echo "Aborted."; exit 0 ;;
esac
exec "${command[@]}"