iscsi/installer: provision merlin d004; fix etc script runtime

- Add merlin iSCSI config for PROLE-DATA-4 mounted at /prole/d004 (xfs, _netdev,noatime)

- Refine iscsi role login/mount flow (device resolution, mkfs-if-missing, UUID fstab, tags)

- Ensure installer run_script stages lib/shell into PROLE_HOME so etc scripts can source common libs; add regression test

- Add init scripts for Forgejo/GitLab; ignore generated conf/prole.cfg

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-03-16 21:30:52 -07:00
parent 5dcd3b9581
commit f051d1d42a
11 changed files with 807 additions and 33 deletions

1
.gitignore vendored
View File

@ -47,6 +47,7 @@ etc/secrets/
*-password.txt
*secret.yaml
.vault_pass
/conf/prole.cfg
/secrets/
/data/
/logs/

8
etc/init_forego.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
# Backward-compat wrapper for a common misspelling.
# Prefer: etc/init_forgejo.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec bash "$SCRIPT_DIR/init_forgejo.sh" "$@"

248
etc/init_forgejo.sh Normal file
View File

@ -0,0 +1,248 @@
#!/usr/bin/env bash
set -euo pipefail
PROG="init_forgejo"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")"
NAMESPACE="${FORGEJO_NAMESPACE:-}"
CFG_PATH=""
FORCE=0
usage() {
cat <<EOF
Usage:
$PROG [options] [deploy]
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: forgejo)
-c, --config <prole.cfg> Path to prole.cfg (defaults to detected)
--force Delete existing Forgejo resources before deploy
--help Show this help
Behavior:
- Applies a minimal Forgejo Deployment/Service manifest.
- Uses CloudNativePG (CNPG) Postgres as the database backend.
- By default, creates a dedicated DB + role inside the CNPG cluster.
EOF
}
die() { echo "[ERROR] $*" >&2; exit 2; }
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$(prole_normalize_mode "${2:-}")"; shift 2 ;;
--mode=*) MODE="$(prole_normalize_mode "${1#*=}")"; shift 1 ;;
-n|--namespace) NAMESPACE="${2:-}"; shift 2 ;;
--namespace=*) NAMESPACE="${1#*=}"; shift 1 ;;
-c|--config) CFG_PATH="${2:-}"; shift 2 ;;
--config=*) CFG_PATH="${1#*=}"; shift 1 ;;
--force) FORCE=1; shift 1 ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
# Resolve config path and namespace defaults from prole.cfg when present
if [[ -z "$CFG_PATH" && -n "${PROLE_CONF:-}" && -f "${PROLE_CONF}/prole.cfg" ]]; then
CFG_PATH="${PROLE_CONF}/prole.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/prole.cfg"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")"
[[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "FORGEJO_NAMESPACE")"
NAMESPACE="$maybe_ns"
fi
NAMESPACE="${NAMESPACE:-forgejo}"
export FORGEJO_NAMESPACE="$NAMESPACE"
case "$MODE" in
k3d|k3s|k8s|local) ;;
*) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;;
esac
export PROLE_MODE="$MODE"
command -v kubectl >/dev/null || die "kubectl not found"
DB_NAMESPACE="${PROLE_NAMESPACE:-}"
if [[ -z "$DB_NAMESPACE" && -n "$CFG_PATH" ]]; then
DB_NAMESPACE="$(_prole_cfg_extract_key "$CFG_PATH" "NAMESPACE")"
fi
DB_NAMESPACE="${DB_NAMESPACE:-default}"
CNPG_CLUSTER_NAME="${CNPG_CLUSTER_NAME:-prole-db}"
FORGEJO_DB_NAME="${FORGEJO_DB_NAME:-forgejo}"
FORGEJO_DB_USER="${FORGEJO_DB_USER:-forgejo}"
FORGEJO_DB_PASSWORD="${FORGEJO_DB_PASSWORD:-}"
if [[ -z "$FORGEJO_DB_PASSWORD" ]]; then
# 24 chars alnum for broad chart compatibility.
FORGEJO_DB_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom 2>/dev/null | head -c 24 || true)
fi
DB_HOST="${CNPG_CLUSTER_NAME}-rw.${DB_NAMESPACE}.svc.cluster.local"
DB_PORT="${FORGEJO_DB_PORT:-5432}"
kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1
if [[ "$FORCE" -eq 1 ]]; then
warn "--force specified; removing existing Forgejo resources in namespace $NAMESPACE"
kubectl -n "$NAMESPACE" delete deploy/forgejo svc/forgejo-http svc/forgejo-ssh ingress/forgejo >/dev/null 2>&1 || true
fi
ensure_db() {
local primary_pod=""
primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${CNPG_CLUSTER_NAME},role=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$primary_pod" ]]; then
primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${CNPG_CLUSTER_NAME}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
if [[ -z "$primary_pod" ]]; then
die "Could not find CNPG pod for cluster '${CNPG_CLUSTER_NAME}' in namespace '${DB_NAMESPACE}'"
fi
log "Ensuring database '${FORGEJO_DB_NAME}' and role '${FORGEJO_DB_USER}' exist in CNPG cluster '${CNPG_CLUSTER_NAME}' (ns=${DB_NAMESPACE})"
# Create role/db idempotently.
kubectl -n "$DB_NAMESPACE" exec "$primary_pod" -- bash -lc "psql -v ON_ERROR_STOP=1 -U postgres -d postgres" <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${FORGEJO_DB_USER}') THEN
CREATE ROLE ${FORGEJO_DB_USER} LOGIN PASSWORD '${FORGEJO_DB_PASSWORD}';
END IF;
END
\$\$;
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '${FORGEJO_DB_NAME}') THEN
CREATE DATABASE ${FORGEJO_DB_NAME} OWNER ${FORGEJO_DB_USER};
END IF;
END
\$\$;
SQL
# Store app connection info in the Forgejo namespace.
kubectl -n "$NAMESPACE" create secret generic forgejo-db \
--from-literal=host="$DB_HOST" \
--from-literal=port="$DB_PORT" \
--from-literal=database="$FORGEJO_DB_NAME" \
--from-literal=username="$FORGEJO_DB_USER" \
--from-literal=password="$FORGEJO_DB_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
}
ensure_db
FORGEJO_IMAGE_REPO="${FORGEJO_IMAGE_REPO:-codeberg.org/forgejo/forgejo}"
FORGEJO_IMAGE_TAG="${FORGEJO_IMAGE_TAG:-latest}"
log "Deploying Forgejo to namespace '$NAMESPACE' (mode=$MODE)"
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: forgejo
spec:
replicas: 1
selector:
matchLabels:
app: forgejo
template:
metadata:
labels:
app: forgejo
spec:
containers:
- name: forgejo
image: ${FORGEJO_IMAGE_REPO}:${FORGEJO_IMAGE_TAG}
ports:
- name: http
containerPort: 3000
- name: ssh
containerPort: 2222
env:
# Prefer Forgejo prefix, but also set Gitea-style keys for compatibility.
- name: FORGEJO__database__DB_TYPE
value: postgres
- name: FORGEJO__database__HOST
valueFrom:
secretKeyRef:
name: forgejo-db
key: host
- name: FORGEJO__database__NAME
valueFrom:
secretKeyRef:
name: forgejo-db
key: database
- name: FORGEJO__database__USER
valueFrom:
secretKeyRef:
name: forgejo-db
key: username
- name: FORGEJO__database__PASSWD
valueFrom:
secretKeyRef:
name: forgejo-db
key: password
- name: GITEA__database__DB_TYPE
value: postgres
- name: GITEA__database__HOST
value: ${DB_HOST}:${DB_PORT}
- name: GITEA__database__NAME
valueFrom:
secretKeyRef:
name: forgejo-db
key: database
- name: GITEA__database__USER
valueFrom:
secretKeyRef:
name: forgejo-db
key: username
- name: GITEA__database__PASSWD
valueFrom:
secretKeyRef:
name: forgejo-db
key: password
---
apiVersion: v1
kind: Service
metadata:
name: forgejo-http
spec:
selector:
app: forgejo
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: forgejo-ssh
spec:
selector:
app: forgejo
ports:
- name: ssh
port: 2222
targetPort: 2222
EOF
log "Waiting for deployment rollout..."
kubectl -n "$NAMESPACE" rollout status deploy/forgejo --timeout=10m
log "Done. Service endpoints:"
kubectl -n "$NAMESPACE" get svc forgejo-http forgejo-ssh

174
etc/init_gitlab.sh Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
PROG="init_gitlab"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/prole_cfg.sh"
MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")"
NAMESPACE="${GITLAB_NAMESPACE:-}"
CFG_PATH=""
FORCE=0
usage() {
cat <<EOF
Usage:
$PROG [options] [deploy]
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: gitlab)
-c, --config <prole.cfg> Path to prole.cfg (defaults to detected)
--force Uninstall existing GitLab release before deploy
--help Show this help
Behavior:
- Installs self-hosted GitLab via the official Helm chart.
- Uses CloudNativePG (CNPG) Postgres as the database backend (external DB).
EOF
}
die() { echo "[ERROR] $*" >&2; exit 2; }
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$(prole_normalize_mode "${2:-}")"; shift 2 ;;
--mode=*) MODE="$(prole_normalize_mode "${1#*=}")"; shift 1 ;;
-n|--namespace) NAMESPACE="${2:-}"; shift 2 ;;
--namespace=*) NAMESPACE="${1#*=}"; shift 1 ;;
-c|--config) CFG_PATH="${2:-}"; shift 2 ;;
--config=*) CFG_PATH="${1#*=}"; shift 1 ;;
--force) FORCE=1; shift 1 ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
# Resolve config path and namespace defaults from prole.cfg when present
if [[ -z "$CFG_PATH" && -n "${PROLE_CONF:-}" && -f "${PROLE_CONF}/prole.cfg" ]]; then
CFG_PATH="${PROLE_CONF}/prole.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/prole.cfg"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")"
[[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITLAB_NAMESPACE")"
NAMESPACE="$maybe_ns"
fi
NAMESPACE="${NAMESPACE:-gitlab}"
export GITLAB_NAMESPACE="$NAMESPACE"
case "$MODE" in
k3d|k3s|k8s|local) ;;
*) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;;
esac
export PROLE_MODE="$MODE"
command -v kubectl >/dev/null || die "kubectl not found"
command -v helm >/dev/null || die "helm not found (required for GitLab install)"
DB_NAMESPACE="${PROLE_NAMESPACE:-}"
if [[ -z "$DB_NAMESPACE" && -n "$CFG_PATH" ]]; then
DB_NAMESPACE="$(_prole_cfg_extract_key "$CFG_PATH" "NAMESPACE")"
fi
DB_NAMESPACE="${DB_NAMESPACE:-default}"
CNPG_CLUSTER_NAME="${CNPG_CLUSTER_NAME:-prole-db}"
GITLAB_DB_NAME="${GITLAB_DB_NAME:-gitlabhq_production}"
GITLAB_DB_USER="${GITLAB_DB_USER:-gitlab}"
GITLAB_DB_PASSWORD="${GITLAB_DB_PASSWORD:-}"
if [[ -z "$GITLAB_DB_PASSWORD" ]]; then
GITLAB_DB_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom 2>/dev/null | head -c 32 || true)
fi
DB_HOST="${CNPG_CLUSTER_NAME}-rw.${DB_NAMESPACE}.svc.cluster.local"
DB_PORT="${GITLAB_DB_PORT:-5432}"
kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1
ensure_db() {
local primary_pod=""
primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${CNPG_CLUSTER_NAME},role=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$primary_pod" ]]; then
primary_pod=$(kubectl -n "$DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${CNPG_CLUSTER_NAME}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
if [[ -z "$primary_pod" ]]; then
die "Could not find CNPG pod for cluster '${CNPG_CLUSTER_NAME}' in namespace '${DB_NAMESPACE}'"
fi
log "Ensuring database '${GITLAB_DB_NAME}' and role '${GITLAB_DB_USER}' exist in CNPG cluster '${CNPG_CLUSTER_NAME}' (ns=${DB_NAMESPACE})"
kubectl -n "$DB_NAMESPACE" exec "$primary_pod" -- bash -lc "psql -v ON_ERROR_STOP=1 -U postgres -d postgres" <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${GITLAB_DB_USER}') THEN
CREATE ROLE ${GITLAB_DB_USER} LOGIN PASSWORD '${GITLAB_DB_PASSWORD}';
END IF;
END
\$\$;
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '${GITLAB_DB_NAME}') THEN
CREATE DATABASE ${GITLAB_DB_NAME} OWNER ${GITLAB_DB_USER};
END IF;
END
\$\$;
SQL
kubectl -n "$NAMESPACE" create secret generic gitlab-db \
--from-literal=host="$DB_HOST" \
--from-literal=port="$DB_PORT" \
--from-literal=database="$GITLAB_DB_NAME" \
--from-literal=username="$GITLAB_DB_USER" \
--from-literal=password="$GITLAB_DB_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
}
ensure_db
RELEASE_NAME="${GITLAB_RELEASE_NAME:-gitlab}"
if [[ "$FORCE" -eq 1 ]]; then
warn "--force specified; uninstalling existing release '$RELEASE_NAME' in namespace '$NAMESPACE' (if present)"
helm -n "$NAMESPACE" uninstall "$RELEASE_NAME" >/dev/null 2>&1 || true
fi
GITLAB_DOMAIN="${GITLAB_DOMAIN:-${DOMAIN:-${PROLE_DOMAIN:-}}}"
GITLAB_DOMAIN="${GITLAB_DOMAIN:-example.local}"
log "Adding/updating GitLab Helm repo"
helm repo add gitlab https://charts.gitlab.io/ >/dev/null 2>&1 || true
helm repo update >/dev/null
log "Installing GitLab (release=$RELEASE_NAME, ns=$NAMESPACE, domain=$GITLAB_DOMAIN)"
# Notes:
# - We keep the chart install minimal to avoid assuming ingress/cert-manager setup.
# - We disable the bundled PostgreSQL and point GitLab to CNPG.
helm upgrade --install "$RELEASE_NAME" gitlab/gitlab \
-n "$NAMESPACE" \
--timeout 30m \
--wait \
--set global.hosts.domain="$GITLAB_DOMAIN" \
--set global.hosts.https=false \
--set certmanager.install=false \
--set prometheus.install=false \
--set postgresql.install=false \
--set global.psql.host="$DB_HOST" \
--set global.psql.port="$DB_PORT" \
--set global.psql.username="$GITLAB_DB_USER" \
--set global.psql.database="$GITLAB_DB_NAME" \
--set global.psql.password.secret=gitlab-db \
--set global.psql.password.key=password
log "Done. Inspect services/pods with: kubectl -n $NAMESPACE get pods,svc"

View File

@ -49,8 +49,19 @@ k3s_service_node_labels: []
# iSCSI
# `/var/lib/rancher` is local host storage (do not manage it via iSCSI).
iscsi_portal: 10.0.0.203
iscsi_portal: 10.0.0.203:3260
iscsi_targets: []
iscsi_targets:
# PROLE-DATA-4
# Repurposed from myrddin's former `/var/lib/rancher` Synology LUN.
- iqn: "iqn.2000-01.com.synology:synology.Target-15.292d45194a1"
chap_user: "prole"
chap_password: "{{ vault_iscsi_prole_password }}"
mounts:
- name: d004
path: /prole/d004
fstype: xfs
opts: "_netdev,noatime"
mkfs_if_missing: true
iscsi_absent_mounts: []

View File

@ -4,6 +4,121 @@
path: "{{ m.path }}"
state: directory
mode: "0755"
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Reset computed mount source
ansible.builtin.set_fact:
_iscsi_mount_src: ""
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Decide whether a block device is required for this mount
ansible.builtin.set_fact:
_iscsi_need_blockdev: >-
{{ (m.mkfs_once | default(false) | bool)
or (m.mkfs_if_missing | default(false) | bool)
or (m.device is defined and (m.device | string | length) > 0)
or (m.src is not defined) or ((m.src | default('') | string | length) == 0) }}
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Parse iSCSI portal (host/port)
ansible.builtin.set_fact:
_iscsi_portal_host: "{{ (iscsi_portal | string).split(':')[0] }}"
_iscsi_portal_port: "{{ ((iscsi_portal | string).split(':') | length > 1) | ternary((iscsi_portal | string).split(':')[1], '3260') }}"
when:
- _iscsi_need_blockdev | default(false) | bool
- iscsi_portal is defined
- (iscsi_portal | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Use explicitly provided block device (if any)
ansible.builtin.set_fact:
_iscsi_blockdev: "{{ m.device }}"
when:
- _iscsi_need_blockdev | default(false) | bool
- m.device is defined
- (m.device | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Compute iSCSI by-path pattern
ansible.builtin.set_fact:
_iscsi_by_path_pattern: >-
ip-{{ _iscsi_portal_host }}:{{ _iscsi_portal_port }}-iscsi-{{ iscsi_target.iqn }}-lun-{{ (m.lun is defined) | ternary((m.lun | string), '*') }}
when:
- _iscsi_need_blockdev | default(false) | bool
- (_iscsi_blockdev is not defined) or ((_iscsi_blockdev | string | length) == 0)
- iscsi_target is defined
- iscsi_target.iqn is defined
- _iscsi_portal_host is defined
- (_iscsi_portal_host | string | length) > 0
- _iscsi_portal_port is defined
- (_iscsi_portal_port | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Wait for iSCSI by-path device to appear
ansible.builtin.find:
paths:
- /dev/disk/by-path
patterns:
- "{{ _iscsi_by_path_pattern }}"
file_type: any
register: _iscsi_by_path_find
until: (_iscsi_by_path_find.matched | default(0) | int) > 0
retries: "{{ ((m.device_timeout | default(60)) | int // 5) + 1 }}"
delay: 5
when:
- _iscsi_need_blockdev | default(false) | bool
- (_iscsi_blockdev is not defined) or ((_iscsi_blockdev | string | length) == 0)
- _iscsi_by_path_pattern is defined
- (_iscsi_by_path_pattern | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Select iSCSI block device from by-path matches
ansible.builtin.set_fact:
_iscsi_blockdev: "{{ (_iscsi_by_path_find.files | map(attribute='path') | list | sort | first) }}"
when:
- _iscsi_need_blockdev | default(false) | bool
- (_iscsi_blockdev is not defined) or ((_iscsi_blockdev | string | length) == 0)
- _iscsi_by_path_find is defined
- (_iscsi_by_path_find.matched | default(0) | int) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Read existing filesystem type (if any)
ansible.builtin.command: "blkid -o value -s TYPE {{ _iscsi_blockdev }}"
register: _iscsi_blkid_type
changed_when: false
failed_when: false
when:
- _iscsi_need_blockdev | default(false) | bool
- _iscsi_blockdev is defined
- (_iscsi_blockdev | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Ensure marker directory exists (mkfs_once)
ansible.builtin.file:
@ -13,22 +128,33 @@
group: root
mode: "0755"
when: m.mkfs_once | default(false) | bool
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Compute mkfs marker path (mkfs_once)
ansible.builtin.set_fact:
_iscsi_mkfs_marker: >-
/var/lib/prole/iscsi/mkfs-
{{ (m.src | regex_replace('^UUID=', 'uuid-')
| regex_replace('^LABEL=', 'label-')
| regex_replace('[^A-Za-z0-9_.-]', '_')) }}
{{ (((iscsi_target.iqn | default('unknown-iqn')) ~ '-' ~ (m.path | default('unknown-path')))
| regex_replace('[^A-Za-z0-9_.-]', '_')) }}
.done
when: m.mkfs_once | default(false) | bool
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Check mkfs marker presence (mkfs_once)
ansible.builtin.stat:
path: "{{ _iscsi_mkfs_marker }}"
register: _iscsi_mkfs_marker_stat
when: m.mkfs_once | default(false) | bool
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Fail if mountpoint is already mounted but mkfs_once requested
ansible.builtin.command: "findmnt -n {{ m.path }}"
@ -38,43 +164,98 @@
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Refuse to mkfs when mountpoint is mounted
ansible.builtin.fail:
msg: "Refusing to mkfs {{ m.src }} because {{ m.path }} is already mounted ({{ _iscsi_findmnt_before_mkfs.stdout | default('') }})."
msg: "Refusing to mkfs for {{ m.path }} because it is already mounted ({{ _iscsi_findmnt_before_mkfs.stdout | default('') }})."
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- _iscsi_findmnt_before_mkfs.rc == 0
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Resolve block device for mkfs (mkfs_once)
ansible.builtin.command: >-
{{ (m.src | regex_search('^UUID=') )
| ternary('blkid -U ' ~ (m.src | regex_replace('^UUID=', '')),
(m.src | regex_search('^LABEL=') )
| ternary('blkid -L ' ~ (m.src | regex_replace('^LABEL=', '')),
'echo ' ~ (m.src))) }}
register: _iscsi_mkfs_dev
changed_when: false
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Require resolved device to look like /dev/* (mkfs_once)
- name: Require iSCSI block device path for mkfs_once
ansible.builtin.assert:
that:
- (_iscsi_mkfs_dev.stdout | default('') | trim) is match('^/dev/')
fail_msg: "Unable to resolve a /dev/* block device for mkfs from src={{ m.src }} (got: {{ _iscsi_mkfs_dev.stdout | default('') | trim }})."
- _iscsi_blockdev is defined
- (_iscsi_blockdev | string | length) > 0
fail_msg: "mkfs_once requested for {{ m.path }} but no block device was computed (set m.device or ensure iscsi_target+iscsi_portal are available)."
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Re-initialize filesystem (mkfs.ext4) (mkfs_once)
ansible.builtin.command: "mkfs.ext4 -F {{ _iscsi_mkfs_dev.stdout | trim }}"
- name: Re-initialize filesystem (mkfs_once)
ansible.builtin.command: >-
{{ ((m.fstype | default('ext4')) == 'xfs')
| ternary(
'mkfs.xfs -f ' ~ _iscsi_blockdev,
(((m.fstype | default('ext4')) == 'ext4')
| ternary(
'mkfs.ext4 -F ' ~ _iscsi_blockdev,
'mkfs -t ' ~ (m.fstype | default('ext4')) ~ ' ' ~ _iscsi_blockdev
)
)
) }}
changed_when: true
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Create filesystem when missing (mkfs_if_missing)
ansible.builtin.command: >-
{{ ((m.fstype | default('ext4')) == 'xfs')
| ternary(
'mkfs.xfs -f ' ~ _iscsi_blockdev,
(((m.fstype | default('ext4')) == 'ext4')
| ternary(
'mkfs.ext4 -F ' ~ _iscsi_blockdev,
'mkfs -t ' ~ (m.fstype | default('ext4')) ~ ' ' ~ _iscsi_blockdev
)
)
) }}
changed_when: true
when:
- m.mkfs_if_missing | default(false) | bool
- _iscsi_blockdev is defined
- (_iscsi_blockdev | string | length) > 0
- _iscsi_blkid_type is not defined or (_iscsi_blkid_type.stdout | default('') | trim) == ''
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Refuse to mount when existing filesystem type does not match
ansible.builtin.fail:
msg: >-
Existing filesystem type on {{ _iscsi_blockdev }} is
'{{ _iscsi_blkid_type.stdout | default('') | trim }}' but mount {{ m.path }}
requests fstype='{{ m.fstype | default('ext4') }}'.
Wipe the device signature (e.g. wipefs) or set mkfs_once=true if you intend
to reformat this LUN.
when:
- _iscsi_blkid_type is defined
- (_iscsi_blkid_type.stdout | default('') | trim) != ''
- (_iscsi_blkid_type.stdout | trim) != (m.fstype | default('ext4'))
- not (m.mkfs_once | default(false) | bool)
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Write mkfs marker (mkfs_once)
ansible.builtin.file:
@ -86,11 +267,64 @@
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
tags:
- iscsi
- iscsi_mkfs
- iscsi_storage
- name: Resolve mount source
ansible.builtin.set_fact:
_iscsi_mount_src: "{{ m.src }}"
when:
- m.src is defined
- (m.src | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Resolve mount source UUID from block device
ansible.builtin.command: "blkid -o value -s UUID {{ _iscsi_blockdev }}"
register: _iscsi_blkid_uuid
changed_when: false
when:
- (_iscsi_mount_src | default('') | string | length) == 0
- _iscsi_blockdev is defined
- (_iscsi_blockdev | string | length) > 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Require resolved UUID for mounting
ansible.builtin.assert:
that:
- _iscsi_blkid_uuid is defined
- (_iscsi_blkid_uuid.stdout | default('') | trim) != ''
fail_msg: "Unable to resolve a filesystem UUID for {{ m.path }} from {{ _iscsi_blockdev }}."
when: (_iscsi_mount_src | default('') | string | length) == 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Set mount source to UUID
ansible.builtin.set_fact:
_iscsi_mount_src: "UUID={{ _iscsi_blkid_uuid.stdout | trim }}"
when: (_iscsi_mount_src | default('') | string | length) == 0
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Mount {{ m.path }}
ansible.builtin.mount:
path: "{{ m.path }}"
src: "{{ m.src }}"
src: "{{ _iscsi_mount_src }}"
fstype: "{{ m.fstype | default('ext4') }}"
opts: "{{ m.opts | default('_netdev,noatime') }}"
state: mounted
tags:
- iscsi
- iscsi_mount
- iscsi_storage

View File

@ -0,0 +1,9 @@
---
- name: Mount target filesystems
ansible.builtin.include_tasks: iscsi_mount.yml
loop: "{{ t.mounts | default([]) }}"
loop_control:
loop_var: m
label: "{{ m.path | default('<unknown-mountpoint>') }}"
vars:
iscsi_target: "{{ t }}"

View File

@ -4,13 +4,33 @@
name:
- open-iscsi
- util-linux
- xfsprogs
state: present
tags:
- iscsi
- iscsi_login
- iscsi_storage
- name: Ensure iscsid enabled
ansible.builtin.service:
name: iscsid
state: started
enabled: true
tags:
- iscsi
- iscsi_login
- iscsi_storage
- name: Discover iSCSI targets
ansible.builtin.command: >-
iscsiadm -m discovery -t st -p {{ iscsi_portal }}
register: _iscsi_discovery
changed_when: false
tags:
- iscsi
- iscsi_discovery
- iscsi_login
- iscsi_storage
- name: Configure and login iSCSI targets
ansible.builtin.include_tasks: iscsi_target.yml
@ -18,3 +38,7 @@
loop_control:
loop_var: t
label: "{{ t.iqn | default('<unknown-iqn>') }}"
tags:
- iscsi
- iscsi_login
- iscsi_storage

View File

@ -17,11 +17,11 @@
| map(attribute='mounts') | list | flatten
| selectattr('path', 'equalto', '/var/lib/rancher')
| list | length) == 0
fail_msg: >-
Inventory attempts to mount `/var/lib/rancher` via iSCSI. K3s state storage is
intended to be local host storage (e.g. USB3 SSD on myrddin). Remove
`/var/lib/rancher` from `iscsi_targets[*].mounts` (or set
`iscsi_allow_rancher_mount: true` if you are intentionally overriding this).
fail_msg: >-
Inventory attempts to mount `/var/lib/rancher` via iSCSI. K3s state storage is
intended to be local host storage (e.g. USB3 SSD on myrddin). Remove
`/var/lib/rancher` from `iscsi_targets[*].mounts` (or set
`iscsi_allow_rancher_mount: true` if you are intentionally overriding this).
when: not (iscsi_allow_rancher_mount | default(false) | bool)
- name: Remove stale iSCSI fstab entries
@ -36,10 +36,15 @@
- iscsi_cleanup
- name: Mount filesystems for iSCSI targets
ansible.builtin.include_tasks: iscsi_mount.yml
loop: "{{ iscsi_targets | default([]) | map(attribute='mounts') | list | flatten }}"
ansible.builtin.include_tasks: iscsi_target_mounts.yml
loop: "{{ iscsi_targets | default([]) }}"
loop_control:
loop_var: m
loop_var: t
label: "{{ t.iqn | default('<unknown-iqn>') }}"
tags:
- iscsi
- iscsi_mount
- iscsi_storage
- name: Guardrail - ensure required mountpoints are mounted
ansible.builtin.command: "findmnt -n {{ item.path }}"
@ -49,3 +54,7 @@
loop: "{{ iscsi_targets | default([]) | map(attribute='mounts') | list | flatten }}"
loop_control:
label: "{{ item.path }}"
tags:
- iscsi
- iscsi_mount
- iscsi_storage

View File

@ -110,6 +110,28 @@ class ProleController:
except Exception:
pass
# Ensure shared shell libraries are available for etc/ scripts.
# Many scripts source `etc/common_core_lib.sh`, which in turn expects
# `../lib/shell/common_core_lib.sh` relative to `$PROLE_HOME/etc`.
try:
source_lib_shell = self.project_root / "lib" / "shell"
target_lib_shell = prole_home / "lib" / "shell"
if source_lib_shell.exists():
target_lib_shell.mkdir(parents=True, exist_ok=True)
for src in source_lib_shell.rglob("*"):
if not src.is_file():
continue
rel = src.relative_to(source_lib_shell)
dst = target_lib_shell / rel
dst.parent.mkdir(parents=True, exist_ok=True)
try:
if (not dst.exists()) or (src.stat().st_mtime > dst.stat().st_mtime):
shutil.copy2(src, dst)
except Exception:
pass
except Exception:
pass
# Copy script and set permissions
if source_script.exists():
if source_script.resolve() != target_script.resolve():

View File

@ -166,6 +166,40 @@ def test_run_script_copies_k8s_and_conf(tmp_path):
assert (target / "conf").exists()
def test_run_script_copies_lib_shell_for_etc_scripts(tmp_path):
"""Regression: etc/ scripts may source ../lib/shell/common_core_lib.sh."""
_scaffold_project(tmp_path)
(tmp_path / "lib" / "shell").mkdir(parents=True, exist_ok=True)
(tmp_path / "lib" / "shell" / "common_core_lib.sh").write_text(
"#!/usr/bin/env bash\ncommon_core_preparse_config() { :; }\n"
)
script = tmp_path / "etc" / "needs_lib.sh"
script.write_text(
"#!/bin/bash\n"
"SCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\n"
"# shellcheck disable=SC1091\n"
"source \"$SCRIPT_DIR/../lib/shell/common_core_lib.sh\"\n"
"common_core_preparse_config\n"
"echo ok\n"
)
target = tmp_path / "prole_home_lib"
target.mkdir()
ctrl = _make_controller(tmp_path)
lines: list[str] = []
rc = ctrl.run_script(
"needs_lib.sh",
env={"PROLE_HOME": str(target)},
on_line=lambda l: lines.append(l),
)
assert rc == 0
assert any("ok" in l for l in lines)
assert (target / "lib" / "shell" / "common_core_lib.sh").exists()
def test_run_script_copies_db_version(tmp_path):
"""Covers the prole-db/.version copy path."""
_scaffold_project(tmp_path)