isoloom_render/
controller.rs1use isoloom_spec::model::{Provision, Spec};
19use isoloom_spec::shell;
20
21const ATTEMPTS: u32 = 4;
23
24const RETRY_DIR: &str = "/tmp/isoloom-retry";
27
28#[must_use]
34pub fn provision_script(spec: &Spec, limit: &[String]) -> String {
35 let limit = if limit.is_empty() {
36 String::new()
37 } else {
38 format!(" --limit {}", shell::quote(&limit.join(",")))
39 };
40 let head = format!(
41 "set -e\nmkdir -p /tmp/isoloom-facts\n{ENVIRONMENT}\nmkdir -p {RETRY_DIR}\nexport ANSIBLE_RETRY_FILES_ENABLED=True ANSIBLE_RETRY_FILES_SAVE_PATH={RETRY_DIR}\n{}",
42 helpers()
43 );
44 let plays: String = spec.provision.iter().map(|step| play(step, &limit)).collect();
45 head + &plays
46}
47
48fn play(step: &Provision, limit: &str) -> String {
51 let (dir, file) = step.ansible.rsplit_once('/').unwrap_or((".", &step.ansible));
52 let extra: String = step.inventory.iter().map(|i| format!(" -i /opt/isoloom/{i}")).collect::<Vec<_>>().concat();
53 let requirements = match &step.requirements {
54 Some(r) => format!("retry_download ansible-galaxy install -r /opt/isoloom/{r}\n"),
55 None => "[ ! -f requirements.yml ] || retry_download ansible-galaxy install -r requirements.yml\n".into(),
56 };
57 format!(
58 "cd /opt/isoloom/{dir}\n{requirements}run_play ansible-playbook -i /etc/isoloom/inventory.ini{extra}{}{limit} {file}\n",
59 vars(step)
60 )
61}
62
63fn vars(step: &Provision) -> String {
65 if step.vars.is_empty() {
66 return String::new();
67 }
68 let json = serde_json::Value::Object(step.vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
69 format!(" -e {}", shell::quote(&json.to_string()))
70}
71
72const ENVIRONMENT: &str = "export PATH=\"/opt/ansible/bin:$PATH\" ANSIBLE_HOST_KEY_CHECKING=False \
79ANSIBLE_GATHERING=smart ANSIBLE_FORKS=20 ANSIBLE_PIPELINING=True ANSIBLE_CACHE_PLUGIN=jsonfile \
80ANSIBLE_CACHE_PLUGIN_CONNECTION=/tmp/isoloom-facts ANSIBLE_CACHE_PLUGIN_TIMEOUT=7200";
81
82const TRANSIENT: &[&str] = &[
88 "send_input failed",
90 "the pipe has been ended",
91 "winrmtransporterror",
92 "winrmoperationtimeouterror",
93 "read timeout",
94 "bad http response returned from server",
95 "(kerberos|ntlm|credssp).*500",
96 "remote end closed connection",
97 "connection reset by peer",
99 "connection refused",
100 "connection aborted",
101 "broken pipe",
102 "timed out",
103 "failed to connect to the host via ssh",
104 "shared connection to .* closed",
105];
106
107fn helpers() -> String {
120 format!(
121 "_isoloom_pause=${{ISOLOOM_RETRY_PAUSE:-15}}\n_isoloom_attempts={ATTEMPTS}\n_isoloom_transient='{}'\n{HELPER_FUNCTIONS}",
122 TRANSIENT.join("|")
123 )
124}
125
126const HELPER_FUNCTIONS: &str = r#"retry_download() {
128 _isoloom_n=1
129 until "$@"; do
130 [ "$_isoloom_n" -lt 5 ] || return 1
131 echo "isoloom: download failed; trying again in $((_isoloom_n * _isoloom_pause)) s ($((_isoloom_n + 1))/5)"
132 sleep $((_isoloom_n * _isoloom_pause))
133 _isoloom_n=$((_isoloom_n + 1))
134 done
135}
136play_hosts() {
137 awk -v transient="$_isoloom_transient" '
138 function commit() {
139 if (pending != "") { if (pending_real) real[pending] = 1; else cut[pending] = 1 }
140 pending = ""
141 }
142 { gsub(/\033\[[0-9;]*m/, "") }
143 /^\.\.\.ignoring/ { pending = ""; next }
144 { commit() }
145 /^(fatal|failed): \[/ && !/UNREACHABLE!/ {
146 pending = $0
147 sub(/^(fatal|failed): \[/, "", pending)
148 sub(/\].*/, "", pending)
149 sub(/ -> .*/, "", pending)
150 pending_real = (tolower($0) !~ transient)
151 next
152 }
153 /^PLAY RECAP/ { recap = 1; next }
154 recap && / : ok=/ {
155 for (i = 2; i <= NF; i++) {
156 split($i, kv, "=")
157 if (kv[1] == "failed" && kv[2] > 0) failed[$1] = 1
158 if (kv[1] == "unreachable" && kv[2] > 0) unreachable[$1] = 1
159 }
160 }
161 END {
162 commit()
163 for (h in failed) print ((h in real || !(h in cut)) ? "real " : "cut ") h
164 for (h in unreachable) if (!(h in failed)) print "unreachable " h
165 }
166 ' "$1" | sort
167}
168_isoloom_kind() {
169 echo "$1" | awk -v k="$2" '$1 == k { printf "%s%s", s, $2; s = " " }'
170}
171run_play() {
172 # The playbook is the last argument; Ansible names its retry file after it.
173 for _isoloom_book; do :; done
174 _isoloom_name=$(basename "$_isoloom_book")
175 _isoloom_retry=${ANSIBLE_RETRY_FILES_SAVE_PATH:-/tmp/isoloom-retry}/${_isoloom_name%.*}.retry
176 _isoloom_limit=
177 _isoloom_log=$(mktemp)
178 _isoloom_rcf=$(mktemp)
179 _isoloom_n=1
180 rm -f "$_isoloom_retry"
181 while :; do
182 { "$@" && echo 0 > "$_isoloom_rcf" || echo $? > "$_isoloom_rcf"; } 2>&1 | tee "$_isoloom_log"
183 _isoloom_rc=$(cat "$_isoloom_rcf")
184 [ "$_isoloom_rc" -ne 0 ] || break
185 _isoloom_hosts=$(play_hosts "$_isoloom_log")
186 _isoloom_real=$(_isoloom_kind "$_isoloom_hosts" real)
187 _isoloom_gone=$(_isoloom_kind "$_isoloom_hosts" unreachable)
188 _isoloom_cut=$(_isoloom_kind "$_isoloom_hosts" cut)
189 _isoloom_again="$_isoloom_gone${_isoloom_gone:+${_isoloom_cut:+ }}$_isoloom_cut"
190 if [ -n "$_isoloom_real" ]; then
191 echo "isoloom: $_isoloom_name: a task failed on $_isoloom_real: not retried"
192 break
193 fi
194 if [ -z "$_isoloom_again" ]; then
195 echo "isoloom: $_isoloom_name failed (exit $_isoloom_rc) with no unreachable host or dropped connection: not retried"
196 break
197 fi
198 if [ "$_isoloom_n" -ge "$_isoloom_attempts" ]; then
199 echo "isoloom: $_isoloom_name: still unreachable after $_isoloom_n runs: $_isoloom_again; giving up"
200 break
201 fi
202 echo "$_isoloom_again" | tr ' ' '\n' > "$_isoloom_retry"
203 if [ -z "$_isoloom_limit" ]; then set -- "$@" --limit "@$_isoloom_retry"; _isoloom_limit=1; fi
204 _isoloom_n=$((_isoloom_n + 1))
205 if [ -n "$_isoloom_gone" ]; then
206 _isoloom_wait=$((4 * _isoloom_pause))
207 echo "isoloom: $_isoloom_name: unreachable: $_isoloom_again; waiting $_isoloom_wait s, then running it again on them ($_isoloom_n/$_isoloom_attempts)"
208 else
209 _isoloom_wait=$((2 * _isoloom_pause))
210 echo "isoloom: $_isoloom_name: the play was cut by a dropped connection on $_isoloom_cut; waiting $_isoloom_wait s, then running it again on them ($_isoloom_n/$_isoloom_attempts)"
211 fi
212 sleep "$_isoloom_wait"
213 done
214 rm -f "$_isoloom_log" "$_isoloom_rcf"
215 [ "$_isoloom_rc" -ne 0 ] || rm -f "$_isoloom_retry"
216 return "$_isoloom_rc"
217}
218"#;
219
220#[cfg(test)]
221mod tests;