173 lines
8.1 KiB
Text
173 lines
8.1 KiB
Text
|
|
# Load previous state
|
||
|
|
if [ -f "$STATE_DIR/.env" ]; then
|
||
|
|
source "$STATE_DIR/.env"
|
||
|
|
fi
|
||
|
|
|
||
|
|
{% if storage_boxes and storage_boxes|length > 0 %}
|
||
|
|
echo ""
|
||
|
|
echo "=== Processing Storage Boxes ==="
|
||
|
|
|
||
|
|
{% for box in storage_boxes %}
|
||
|
|
echo ""
|
||
|
|
echo "Processing storage box: {{ box.name }} (type={{ box.type | default(value='bx11') }})"
|
||
|
|
|
||
|
|
{# ── Step 1: Create if absent (idempotent: describe-then-create) ───────── #}
|
||
|
|
if hcloud storage-box describe "{{ box.name }}" &>/dev/null; then
|
||
|
|
echo " Storage Box '{{ box.name }}' already exists, skipping creation"
|
||
|
|
else
|
||
|
|
: "${STORAGEBOX_PASSWORD:?STORAGEBOX_PASSWORD must be set (inject from SOPS) before creating {{ box.name }}}"
|
||
|
|
hcloud storage-box create \
|
||
|
|
--name "{{ box.name }}" \
|
||
|
|
--type "{{ box.type | default(value='bx11') }}" \
|
||
|
|
--location "{{ box.location | default(value='fsn1') }}" \
|
||
|
|
--password "$STORAGEBOX_PASSWORD" \
|
||
|
|
{%- if box.enable_ssh | default(value=true) %}
|
||
|
|
--enable-ssh \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if box.enable_samba | default(value=false) %}
|
||
|
|
--enable-samba \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if box.enable_webdav | default(value=false) %}
|
||
|
|
--enable-webdav \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if box.enable_zfs | default(value=false) %}
|
||
|
|
--enable-zfs \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if box.reachable_externally | default(value=false) %}
|
||
|
|
--reachable-externally \
|
||
|
|
{%- endif %}
|
||
|
|
{%- for key in box.ssh_keys | default(value=[]) %}
|
||
|
|
--ssh-key "{{ key }}" \
|
||
|
|
{%- endfor %}
|
||
|
|
{%- if box.labels is defined %}
|
||
|
|
{%- for k, v in box.labels %}
|
||
|
|
--label "{{ k }}={{ v }}" \
|
||
|
|
{%- endfor %}
|
||
|
|
{%- endif %}
|
||
|
|
--output json > "$STATE_DIR/storagebox_{{ box.name }}.json"
|
||
|
|
echo " ✓ Storage Box '{{ box.name }}' created ({{ box.type | default(value='bx11') }})"
|
||
|
|
fi
|
||
|
|
|
||
|
|
{# ── Step 1b: Reconcile access settings — runs unconditionally (create or
|
||
|
|
already-existed) so protocol toggles in servers.ncl take effect on boxes
|
||
|
|
that already exist, not just at creation. hcloud storage-box
|
||
|
|
update-access-settings applies live, no reboot/downtime. #}
|
||
|
|
_access_out=$(hcloud storage-box update-access-settings "{{ box.name }}" \
|
||
|
|
--enable-ssh={{ box.enable_ssh | default(value=true) }} \
|
||
|
|
--enable-samba={{ box.enable_samba | default(value=false) }} \
|
||
|
|
--enable-webdav={{ box.enable_webdav | default(value=false) }} \
|
||
|
|
--enable-zfs={{ box.enable_zfs | default(value=false) }} \
|
||
|
|
--reachable-externally={{ box.reachable_externally | default(value=false) }} 2>&1)
|
||
|
|
if [ $? -eq 0 ]; then
|
||
|
|
echo " ✓ access settings reconciled (ssh={{ box.enable_ssh | default(value=true) }} samba={{ box.enable_samba | default(value=false) }} webdav={{ box.enable_webdav | default(value=false) }} external={{ box.reachable_externally | default(value=false) }})"
|
||
|
|
else
|
||
|
|
echo " ⚠ could not reconcile access settings for {{ box.name }}: $_access_out"
|
||
|
|
fi
|
||
|
|
|
||
|
|
{# ── Step 2: Delete protection ─────────────────────────────────────────── #}
|
||
|
|
{%- if box.protection is defined and box.protection.delete %}
|
||
|
|
_protect_out=$(hcloud storage-box enable-protection "{{ box.name }}" delete 2>&1)
|
||
|
|
if [ $? -eq 0 ]; then
|
||
|
|
echo " ✓ delete protection enabled"
|
||
|
|
else
|
||
|
|
echo " ⚠ could not set delete protection (already set?): $_protect_out"
|
||
|
|
fi
|
||
|
|
{%- endif %}
|
||
|
|
|
||
|
|
{# ── Step 3: Subaccounts — per-purpose scoped access (protocol + path +
|
||
|
|
credential). password_env_key is mandatory and explicit (not derived from
|
||
|
|
home_directory) — each subaccount gets its OWN STORAGEBOX_SUB_PASSWORD_<key>
|
||
|
|
env var, so a leaked SMB credential never also grants the restic SFTP path. #}
|
||
|
|
{%- for sub in box.subaccounts | default(value=[]) %}
|
||
|
|
echo " Subaccount home={{ sub.home_directory }} (readonly={{ sub.readonly | default(value=false) }})"
|
||
|
|
_sub_exists=$(HJSON="$(hcloud storage-box subaccount list "{{ box.name }}" -o json 2>/dev/null)" \
|
||
|
|
nu -c '$env.HJSON | from json | (any {|s| ($s | get -o home_directory | default "") == "{{ sub.home_directory }}"}) | if $in { "True" } else { "False" }' 2>/dev/null || echo "False")
|
||
|
|
if [ "$_sub_exists" = "True" ]; then
|
||
|
|
echo " subaccount for {{ sub.home_directory }} already exists, skipping"
|
||
|
|
else
|
||
|
|
: "${STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }}:?STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }} must be set (inject from SOPS) for subaccount {{ sub.home_directory }} on {{ box.name }}}"
|
||
|
|
# stdout+stderr merged into a variable (not streamed) — the nu wrapper that
|
||
|
|
# runs this script only surfaces the bash process's OWN stderr when the
|
||
|
|
# WHOLE script exits non-zero. A single failed subaccount must not abort
|
||
|
|
# the rest of the reconcile, so the script keeps going — which means its
|
||
|
|
# real error text has to be echoed inline (into stdout) or it's silently
|
||
|
|
# dropped by the caller. See qa: storage-box-subaccount-error-swallowed.
|
||
|
|
_create_out=$(hcloud storage-box subaccount create "{{ box.name }}" \
|
||
|
|
--home-directory "{{ sub.home_directory }}" \
|
||
|
|
--name "{{ sub.name | default(value=sub.home_directory) }}" \
|
||
|
|
--password "$STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }}" \
|
||
|
|
{%- if sub.enable_ssh | default(value=true) %}
|
||
|
|
--enable-ssh \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if sub.enable_samba | default(value=false) %}
|
||
|
|
--enable-samba \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if sub.enable_webdav | default(value=false) %}
|
||
|
|
--enable-webdav \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if sub.reachable_externally | default(value=false) %}
|
||
|
|
--reachable-externally \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if sub.readonly | default(value=false) %}
|
||
|
|
--readonly \
|
||
|
|
{%- endif %}
|
||
|
|
{%- if sub.description %}
|
||
|
|
--description "{{ sub.description }}" \
|
||
|
|
{%- endif %}
|
||
|
|
--output json 2>&1)
|
||
|
|
_create_rc=$?
|
||
|
|
if [ $_create_rc -eq 0 ]; then
|
||
|
|
echo "$_create_out" >> "$STATE_DIR/storagebox_{{ box.name }}_subs.json"
|
||
|
|
echo " ✓ subaccount created for {{ sub.home_directory }}"
|
||
|
|
else
|
||
|
|
echo " ❌ subaccount create FAILED for {{ sub.home_directory }}: $_create_out"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
{# ── Reconcile subaccount access settings — runs unconditionally, same
|
||
|
|
reasoning as Step 1b. NOTE: the subaccount identifier field name below
|
||
|
|
(tries username, then id) is not verified against a live box — confirm
|
||
|
|
with `hcloud storage-box subaccount list "{{ box.name }}" -o json` on
|
||
|
|
first real run and adjust the extraction if it differs. #}
|
||
|
|
_sub_ident=$(HJSON="$(hcloud storage-box subaccount list "{{ box.name }}" -o json 2>/dev/null)" \
|
||
|
|
nu -c '$env.HJSON | from json | where home_directory == "{{ sub.home_directory }}" | get -o 0 | if ($in == null) { "" } else { let m = $in; ($m | get -o username | default ($m | get -o id | default "" | into string)) }' 2>/dev/null)
|
||
|
|
if [ -n "$_sub_ident" ]; then
|
||
|
|
_reconcile_out=$(hcloud storage-box subaccount update-access-settings "{{ box.name }}" "$_sub_ident" \
|
||
|
|
--enable-ssh={{ sub.enable_ssh | default(value=true) }} \
|
||
|
|
--enable-samba={{ sub.enable_samba | default(value=false) }} \
|
||
|
|
--enable-webdav={{ sub.enable_webdav | default(value=false) }} \
|
||
|
|
--reachable-externally={{ sub.reachable_externally | default(value=false) }} \
|
||
|
|
--readonly={{ sub.readonly | default(value=false) }} 2>&1)
|
||
|
|
if [ $? -eq 0 ]; then
|
||
|
|
echo " ✓ access settings reconciled for {{ sub.home_directory }}"
|
||
|
|
else
|
||
|
|
echo " ⚠ could not reconcile access settings for {{ sub.home_directory }}: $_reconcile_out"
|
||
|
|
fi
|
||
|
|
{# name/description are set only at creation by `subaccount create`; `update`
|
||
|
|
keeps them in sync declaratively for already-existing subaccounts. #}
|
||
|
|
_meta_out=$(hcloud storage-box subaccount update "{{ box.name }}" "$_sub_ident" \
|
||
|
|
--name "{{ sub.name | default(value=sub.home_directory) }}" \
|
||
|
|
{%- if sub.description %}
|
||
|
|
--description "{{ sub.description }}" \
|
||
|
|
{%- endif %}
|
||
|
|
2>&1)
|
||
|
|
if [ $? -eq 0 ]; then
|
||
|
|
echo " ✓ name/description reconciled for {{ sub.home_directory }}"
|
||
|
|
else
|
||
|
|
echo " ⚠ could not reconcile name/description for {{ sub.home_directory }}: $_meta_out"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
echo " ⚠ could not resolve subaccount identifier for {{ sub.home_directory }} — skipping reconcile"
|
||
|
|
fi
|
||
|
|
{%- endfor %}
|
||
|
|
|
||
|
|
_sb_key=$(echo "{{ box.name }}" | tr 'a-z' 'A-Z' | tr -c 'A-Z0-9' '_')
|
||
|
|
echo "STORAGEBOX_${_sb_key}=ready" >> "$STATE_DIR/.env"
|
||
|
|
{% endfor %}
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
echo "=== Storage Box Deployment Complete ==="
|
||
|
|
{% else %}
|
||
|
|
echo "No storage boxes defined for this configuration."
|
||
|
|
{% endif %}
|