263 lines
7.6 KiB
Go
263 lines
7.6 KiB
Go
package plan
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"metis/pkg/config"
|
|
"metis/pkg/secrets"
|
|
)
|
|
|
|
func cloudInitUserData(cfg *config.NodeConfig, sec *secrets.NodeSecrets) string {
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
if sec != nil && sec.CloudInit != "" {
|
|
return sec.CloudInit
|
|
}
|
|
var b bytes.Buffer
|
|
b.WriteString("#cloud-config\n")
|
|
b.WriteString(fmt.Sprintf("hostname: %s\n", cfg.Hostname))
|
|
if len(cfg.SSHKeys) > 0 {
|
|
b.WriteString("ssh_authorized_keys:\n")
|
|
for _, k := range cfg.SSHKeys {
|
|
b.WriteString(fmt.Sprintf(" - %s\n", k))
|
|
}
|
|
}
|
|
if hasNodePasswords(sec) {
|
|
b.WriteString("ssh_pwauth: true\n")
|
|
b.WriteString("disable_root: false\n")
|
|
}
|
|
b.WriteString("runcmd:\n")
|
|
b.WriteString(" - [bash, -lc, \"/usr/local/sbin/metis-apply-node-identity.sh\"]\n")
|
|
return b.String()
|
|
}
|
|
|
|
func firstbootEnvContent(cfg *config.NodeConfig, sec *secrets.NodeSecrets) string {
|
|
var b bytes.Buffer
|
|
b.WriteString(fmt.Sprintf("METIS_HOSTNAME=%s\n", shellQuote(cfg.Hostname)))
|
|
b.WriteString(fmt.Sprintf("METIS_SSH_USER=%s\n", shellQuote(cfg.SSHUser)))
|
|
b.WriteString("METIS_ATLAS_USER='atlas'\n")
|
|
b.WriteString(fmt.Sprintf("METIS_K3S_VERSION=%s\n", shellQuote(cfg.K3s.Version)))
|
|
if sec != nil {
|
|
if value := effectiveAtlasPassword(sec); value != "" {
|
|
b.WriteString(fmt.Sprintf("METIS_ATLAS_PASSWORD=%s\n", shellQuote(value)))
|
|
}
|
|
if value := effectiveAtlasPasswordHash(sec); value != "" {
|
|
b.WriteString(fmt.Sprintf("METIS_ATLAS_PASSWORD_HASH=%s\n", shellQuote(value)))
|
|
}
|
|
if value := strings.TrimSpace(sec.RootPassword); value != "" {
|
|
b.WriteString(fmt.Sprintf("METIS_ROOT_PASSWORD=%s\n", shellQuote(value)))
|
|
}
|
|
if value := strings.TrimSpace(sec.RootPasswordHash); value != "" {
|
|
b.WriteString(fmt.Sprintf("METIS_ROOT_PASSWORD_HASH=%s\n", shellQuote(value)))
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func cloudInitRootFSContent(sec *secrets.NodeSecrets) string {
|
|
var b bytes.Buffer
|
|
b.WriteString("#cloud-config\n")
|
|
if hasNodePasswords(sec) {
|
|
b.WriteString("ssh_pwauth: true\n")
|
|
b.WriteString("disable_root: false\n")
|
|
}
|
|
b.WriteString("runcmd:\n")
|
|
b.WriteString(" - [bash, -lc, \"/usr/local/sbin/metis-apply-node-identity.sh\"]\n")
|
|
return b.String()
|
|
}
|
|
|
|
func nodeIdentityScriptContent() string {
|
|
return `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
marker="/var/lib/metis/node-identity-applied.done"
|
|
env_file="/etc/metis/firstboot.env"
|
|
key_file="/etc/metis/authorized_keys"
|
|
sudoers_file="/etc/metis/sudoers-hecate"
|
|
default_groups=(adm sudo tty disk dialout audio video plugdev games users systemd-journal input render netdev)
|
|
|
|
if [ -f "${marker}" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p /var/lib/metis
|
|
if [ -f "${env_file}" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "${env_file}"
|
|
fi
|
|
|
|
atlas_user="${METIS_ATLAS_USER:-atlas}"
|
|
ssh_user="${METIS_SSH_USER:-${atlas_user}}"
|
|
atlas_password="${METIS_ATLAS_PASSWORD:-}"
|
|
atlas_password_hash="${METIS_ATLAS_PASSWORD_HASH:-}"
|
|
root_password="${METIS_ROOT_PASSWORD:-}"
|
|
root_password_hash="${METIS_ROOT_PASSWORD_HASH:-}"
|
|
|
|
group_list=()
|
|
for group_name in "${default_groups[@]}"; do
|
|
if getent group "${group_name}" >/dev/null 2>&1; then
|
|
group_list+=("${group_name}")
|
|
fi
|
|
done
|
|
if [ "${#group_list[@]}" -gt 0 ]; then
|
|
group_csv="$(IFS=,; printf '%s' "${group_list[*]}")"
|
|
else
|
|
group_csv=""
|
|
fi
|
|
|
|
ensure_user() {
|
|
local user_name="$1"
|
|
[ -n "${user_name}" ] || return 0
|
|
if ! id "${user_name}" >/dev/null 2>&1; then
|
|
if [ -n "${group_csv}" ]; then
|
|
useradd -m -s /bin/bash -G "${group_csv}" "${user_name}"
|
|
else
|
|
useradd -m -s /bin/bash "${user_name}"
|
|
fi
|
|
elif [ -n "${group_csv}" ]; then
|
|
usermod -a -G "${group_csv}" "${user_name}" || true
|
|
fi
|
|
}
|
|
|
|
apply_password() {
|
|
local user_name="$1"
|
|
local plain_password="$2"
|
|
local hash_password="$3"
|
|
if ! id "${user_name}" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if [ -n "${hash_password}" ]; then
|
|
usermod -p "${hash_password}" "${user_name}"
|
|
passwd -u "${user_name}" >/dev/null 2>&1 || true
|
|
return 0
|
|
fi
|
|
if [ -n "${plain_password}" ]; then
|
|
printf '%s:%s\n' "${user_name}" "${plain_password}" | chpasswd
|
|
passwd -u "${user_name}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
install_keys() {
|
|
local user_name="$1"
|
|
[ -n "${user_name}" ] || return 0
|
|
[ -s "${key_file}" ] || return 0
|
|
local home_dir
|
|
home_dir="$(getent passwd "${user_name}" | cut -d: -f6)"
|
|
if [ -z "${home_dir}" ]; then
|
|
if [ "${user_name}" = "root" ]; then
|
|
home_dir="/root"
|
|
else
|
|
home_dir="/home/${user_name}"
|
|
fi
|
|
fi
|
|
install -d -m 700 "${home_dir}/.ssh"
|
|
install -m 600 "${key_file}" "${home_dir}/.ssh/authorized_keys"
|
|
chown -R "${user_name}:${user_name}" "${home_dir}/.ssh" 2>/dev/null || true
|
|
}
|
|
|
|
ensure_user "${atlas_user}"
|
|
if [ -n "${ssh_user}" ] && [ "${ssh_user}" != "root" ] && [ "${ssh_user}" != "${atlas_user}" ]; then
|
|
ensure_user "${ssh_user}"
|
|
fi
|
|
|
|
apply_password root "${root_password}" "${root_password_hash}"
|
|
apply_password "${atlas_user}" "${atlas_password}" "${atlas_password_hash}"
|
|
if [ -n "${ssh_user}" ] && [ "${ssh_user}" != "root" ] && [ "${ssh_user}" != "${atlas_user}" ]; then
|
|
apply_password "${ssh_user}" "${atlas_password}" "${atlas_password_hash}"
|
|
fi
|
|
|
|
if [ -s "${key_file}" ]; then
|
|
install_keys root
|
|
install_keys "${atlas_user}"
|
|
if [ -n "${ssh_user}" ] && [ "${ssh_user}" != "root" ] && [ "${ssh_user}" != "${atlas_user}" ]; then
|
|
install_keys "${ssh_user}"
|
|
fi
|
|
fi
|
|
|
|
if [ -s "${sudoers_file}" ]; then
|
|
install -d -m 755 /etc/sudoers.d
|
|
install -m 440 "${sudoers_file}" /etc/sudoers.d/90-hecate-atlas
|
|
if command -v visudo >/dev/null 2>&1; then
|
|
visudo -cf /etc/sudoers.d/90-hecate-atlas >/dev/null 2>&1 || rm -f /etc/sudoers.d/90-hecate-atlas
|
|
fi
|
|
fi
|
|
|
|
systemctl restart ssh.service >/dev/null 2>&1 || systemctl restart sshd.service >/dev/null 2>&1 || systemctl restart ssh.socket >/dev/null 2>&1 || true
|
|
touch "${marker}"
|
|
`
|
|
}
|
|
|
|
func sshPasswordConfigContent(sec *secrets.NodeSecrets) string {
|
|
if !hasNodePasswords(sec) {
|
|
return ""
|
|
}
|
|
return "PasswordAuthentication yes\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nPermitRootLogin yes\nUsePAM yes\n"
|
|
}
|
|
|
|
func hasNodePasswords(sec *secrets.NodeSecrets) bool {
|
|
if sec == nil {
|
|
return false
|
|
}
|
|
return effectiveAtlasPassword(sec) != "" || effectiveAtlasPasswordHash(sec) != "" || firstNonEmptyString(sec.RootPassword, sec.RootPasswordHash) != ""
|
|
}
|
|
|
|
func effectiveAtlasPassword(sec *secrets.NodeSecrets) string {
|
|
if sec == nil {
|
|
return ""
|
|
}
|
|
return firstNonEmptyString(sec.AtlasPassword, sec.SSHPassword)
|
|
}
|
|
|
|
func effectiveAtlasPasswordHash(sec *secrets.NodeSecrets) string {
|
|
if sec == nil {
|
|
return ""
|
|
}
|
|
return firstNonEmptyString(sec.AtlasPasswordHash, sec.SSHPasswordHash)
|
|
}
|
|
|
|
func firstNonEmptyString(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func redactedSecretsForImage(sec *secrets.NodeSecrets) map[string]any {
|
|
if sec == nil {
|
|
return nil
|
|
}
|
|
debug := map[string]any{
|
|
"has_ssh_password": firstNonEmptyString(sec.SSHPassword, sec.SSHPasswordHash) != "",
|
|
"has_atlas_password": firstNonEmptyString(sec.AtlasPassword, sec.AtlasPasswordHash) != "",
|
|
"has_root_password": firstNonEmptyString(sec.RootPassword, sec.RootPasswordHash) != "",
|
|
"has_k3s_token": strings.TrimSpace(sec.K3sToken) != "",
|
|
"has_cloud_init_override": strings.TrimSpace(sec.CloudInit) != "",
|
|
}
|
|
if len(sec.Extra) > 0 {
|
|
keys := make([]string, 0, len(sec.Extra))
|
|
for key := range sec.Extra {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
debug["extra_keys"] = keys
|
|
}
|
|
return debug
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
if value == "" {
|
|
return "''"
|
|
}
|
|
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
|
}
|