#!/usr/bin/env bash
# Ubuntu Server 18.04 a 26.04. Publicar como arquivo estatico, sem senha embutida.
# Uso: sudo bash dedicado.sh 'NOVA_SENHA'   ou   sudo bash dedicado.sh
set +x
set -Eeuo pipefail
umask 077
export PATH=/usr/sbin:/usr/bin:/sbin:/bin

log() { printf '%s\n' "$*"; }
die() { printf 'ERRO: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die 'Execute com sudo ou como root.'
[[ $# -le 1 ]] || die "Uso: bash dedicado.sh ['NOVA_SENHA']"
source /etc/os-release
[[ ${ID:-} == ubuntu ]] || die 'Este script exige Ubuntu.'
dpkg --compare-versions "${VERSION_ID:-0}" ge 18.04 &&
dpkg --compare-versions "${VERSION_ID:-0}" le 26.04 ||
    die "Versao nao suportada: ${VERSION_ID:-desconhecida}."
log "Ubuntu detectado: $VERSION_ID"
for cmd in netplan python3 chpasswd sshd systemctl timeout flock; do
    command -v "$cmd" >/dev/null || die "Comando ausente: $cmd. Instale-o antes de continuar."
done
exec 9>/run/lock/dedicado.lock
flock -n 9 || die 'Outra execucao deste script esta ativa.'
[[ -f /etc/ssh/sshd_config && ! -L /etc/ssh/sshd_config ]] ||
    die 'sshd_config ausente ou link simbolico; revise manualmente.'
systemctl is-active --quiet ssh || die 'O servico ssh precisa estar ativo.'
sshd -t || die 'A configuracao SSH atual ja apresenta erros.'
root_shell=$(getent passwd root | cut -d: -f7)
[[ -x $root_shell && $root_shell != */nologin && $root_shell != */false ]] ||
    die 'O root nao possui um shell de login valido.'

shopt -s nullglob
files=(/etc/netplan/*.yaml /etc/netplan/*.yml)
[[ ${#files[@]} -gt 0 ]] || die 'Nenhum YAML encontrado em /etc/netplan.'
found=0
for name in 50-cloud-init.yaml 01-netcfg.yaml 00-installer-config.yaml; do
    if [[ -f /etc/netplan/$name ]]; then
        log "Netplan identificado: /etc/netplan/$name"
        found=1
    fi
done
[[ $found -eq 1 ]] || die 'Nenhum dos tres nomes de arquivo esperados foi encontrado.'
# Outras camadas podem concatenar listas de DNS. Exigir revisao nesses casos.
for dir in /run/netplan /lib/netplan; do
    extra=("$dir"/*.yaml "$dir"/*.yml)
    [[ ${#extra[@]} -eq 0 ]] || die "Ha configuracao adicional em $dir; revise antes de executar."
done
for file in "${files[@]}"; do
    [[ -f $file && ! -L $file && $file != *$'\n'* ]] ||
        die "Arquivo Netplan nao suportado: $file"
done

password=${1-}
set --
if [[ -z $password ]]; then
    [[ -r /dev/tty ]] || die 'Informe a senha como argumento ou execute em terminal interativo.'
    IFS= read -r -s -p 'Nova senha do root: ' password </dev/tty
    printf '\n' >/dev/tty
    IFS= read -r -s -p 'Confirme a senha: ' confirmation </dev/tty
    printf '\n' >/dev/tty
    [[ $password == "$confirmation" ]] || die 'As senhas nao coincidem.'
    unset confirmation
fi
[[ -n $password && $password != *$'\n'* && $password != *$'\r'* ]] ||
    die 'A senha nao pode ser vazia nem conter quebra de linha.'

if ! python3 -c 'import yaml' 2>/dev/null; then
    log 'Instalando a dependencia python3-yaml...'
    apt-get update
    DEBIAN_FRONTEND=noninteractive apt-get install -y python3-yaml
fi

backup=$(mktemp -d /root/dedicado-backup-XXXXXXXX)
mkdir -p "$backup/original" "$backup/stage"
cloud=/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
changed=("${files[@]}" /etc/ssh/sshd_config)
if [[ -d /etc/cloud/cloud.cfg.d ]]; then
    [[ ! -L $cloud ]] || die "Link simbolico nao suportado: $cloud"
    changed+=("$cloud")
fi
for file in "${changed[@]}"; do
    if [[ -e $file ]]; then
        cp -a --parents -- "$file" "$backup/original/"
    fi
done
log "Backup privado: $backup/original"
modified=0
password_changed=0
rollback() {
    local status=$?
    trap - EXIT HUP INT TERM
    unset password
    if [[ $status -ne 0 && $modified -eq 1 ]]; then
        log 'Falha: restaurando os arquivos de configuracao anteriores.' >&2
        for file in "${changed[@]}"; do
            if [[ -e $backup/original$file ]]; then
                cp -a -- "$backup/original$file" "$file" || true
            else
                rm -f -- "$file" || true
            fi
        done
        netplan generate && timeout 60 netplan apply ||
            log 'ATENCAO: revise a rede pelo console do provedor.' >&2
        sshd -t && systemctl reload ssh ||
            log 'ATENCAO: revise o SSH pelo console do provedor.' >&2
        if [[ $password_changed -eq 1 ]]; then
            log 'A senha do root ja foi alterada e NAO foi revertida.' >&2
        fi
    fi
    exit "$status"
}
trap rollback EXIT
trap 'exit 130' INT
trap 'exit 143' HUP TERM

python3 - "$backup/stage" "${files[@]}" <<'PY'
import copy
import os
import sys
import yaml

# Recusar chaves duplicadas para nao descartar configuracao silenciosamente.
class Loader(yaml.SafeLoader):
    pass

def mapping(loader, node, deep=False):
    loader.flatten_mapping(node)
    result = {}
    for key_node, value_node in node.value:
        key = loader.construct_object(key_node, deep=deep)
        if key in result:
            raise ValueError('Chave YAML duplicada: {}'.format(key))
        result[key] = loader.construct_object(value_node, deep=deep)
    return result

Loader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, mapping)
stage = sys.argv[1]
count = 0
for path in sys.argv[2:]:
    with open(path, encoding='utf-8') as stream:
        data = yaml.load(stream, Loader=Loader)
    if not isinstance(data, dict) or not isinstance(data.get('network'), dict):
        raise ValueError('Estrutura network invalida: ' + path)
    before = copy.deepcopy(data)
    network = data['network']
    if network.get('version') != 2:
        raise ValueError('Exige Netplan version: 2 em ' + path)
    for section in ('ethernets', 'wifis', 'bridges', 'bonds', 'vlans', 'tunnels'):
        for name, config in network.get(section, {}).items():
            if not isinstance(config, dict):
                raise ValueError('Interface invalida: ' + str(name))
            # Interfaces escravas sem IP/DHCP/DNS continuam sem DNS proprio.
            if not any(k in config for k in ('addresses', 'dhcp4', 'dhcp6', 'nameservers')):
                continue
            servers = config.setdefault('nameservers', {})
            if not isinstance(servers, dict):
                raise ValueError('nameservers invalido em ' + str(name))
            servers['addresses'] = ['8.8.8.8', '8.8.4.4']
            renderer = config.get('renderer', network.get('renderer', 'networkd'))
            for family in ('dhcp4', 'dhcp6'):
                if config.get(family) is True:
                    if renderer != 'networkd':
                        raise ValueError('DHCP com renderer diferente de networkd requer revisao: ' + str(name))
                    config.setdefault(family + '-overrides', {})['use-dns'] = False
            count += 1
            print('DNS Google: {} / {}'.format(os.path.basename(path), name))
    # Verificar semanticamente que somente os campos de DNS foram alterados.
    check = copy.deepcopy(data)
    for section in ('ethernets', 'wifis', 'bridges', 'bonds', 'vlans', 'tunnels'):
        for name, original in before['network'].get(section, {}).items():
            current = check['network'][section][name]
            for key in ('nameservers', 'dhcp4-overrides', 'dhcp6-overrides'):
                if key in original:
                    current[key] = original[key]
                else:
                    current.pop(key, None)
    if check != before:
        raise ValueError('Alteracao inesperada fora de DNS: ' + path)
    output = stage + path
    os.makedirs(os.path.dirname(output), exist_ok=True)
    with open(output, 'w', encoding='utf-8') as stream:
        # safe_dump tambem funciona com o PyYAML fornecido no Ubuntu 18.04.
        yaml.safe_dump(data, stream, default_flow_style=False, allow_unicode=True)
if count == 0:
    raise ValueError('Nenhuma interface com IP, DHCP ou DNS encontrada.')
PY

python3 - "$backup/stage/etc/ssh/sshd_config" <<'PY'
import os
import sys
start = '# BEGIN dedicado.sh managed root access'
end = '# END dedicado.sh managed root access'
with open('/etc/ssh/sshd_config', encoding='utf-8') as stream:
    lines = stream.read().splitlines()
body = []
inside = False
for line in lines:
    if line == start:
        if inside:
            raise ValueError('Bloco SSH gerenciado duplicado')
        inside = True
    elif line == end:
        if not inside:
            raise ValueError('Bloco SSH gerenciado invalido')
        inside = False
    elif not inside:
        body.append(line)
if inside:
    raise ValueError('Bloco SSH gerenciado incompleto')
os.makedirs(os.path.dirname(sys.argv[1]), exist_ok=True)
with open(sys.argv[1], 'w', encoding='utf-8') as stream:
    # Antes de Include: a primeira ocorrencia global prevalece no OpenSSH.
    stream.write(start + '\nPermitRootLogin yes\nPasswordAuthentication yes\n' + end + '\n')
    stream.write('\n'.join(body) + '\n')
PY

if [[ -d /etc/cloud/cloud.cfg.d ]]; then
    mkdir -p "$backup/stage/etc/cloud/cloud.cfg.d"
    # Preservar outras opcoes que ja existam neste arquivo.
    python3 - "$cloud" "$backup/stage$cloud" <<'PY'
import os
import sys
import yaml
data = {}
if os.path.exists(sys.argv[1]):
    with open(sys.argv[1], encoding='utf-8') as stream:
        data = yaml.safe_load(stream) or {}
if not isinstance(data, dict):
    raise ValueError('Configuracao cloud-init invalida')
data['network'] = {'config': 'disabled'}
with open(sys.argv[2], 'w', encoding='utf-8') as stream:
    yaml.safe_dump(data, stream, default_flow_style=False)
PY
fi

modified=1
for file in "${changed[@]}"; do
    if [[ $file == /etc/ssh/sshd_config ]]; then
        cat "$backup/stage$file" > "$file"
    else
        install -m 600 -- "$backup/stage$file" "$file"
    fi
done

log 'Validando Netplan e SSH antes da aplicacao...'
netplan generate
sshd -t
client_ip=${SSH_CONNECTION:-}
client_ip=${client_ip%% *}
client_ip=${client_ip:-127.0.0.1}
effective=$(sshd -T -C "user=root,host=$client_ip,addr=$client_ip")
grep -qx 'permitrootlogin yes' <<< "$effective" || die 'Uma regra Match impede login do root.'
grep -qx 'passwordauthentication yes' <<< "$effective" || die 'Uma regra Match impede autenticacao por senha.'
grep -Eq '^authenticationmethods (any|password)$' <<< "$effective" ||
    die 'AuthenticationMethods exige outros fatores. Revise manualmente antes de continuar.'
if grep -Eq '^(allowusers|denyusers|allowgroups|denygroups) ' <<< "$effective"; then
    die 'Ha regras Allow/Deny no SSH. Revise a autorizacao do root antes de continuar.'
fi

log 'Aplicando DNS...'
timeout 60 netplan apply
log 'Definindo a senha do root...'
printf 'root:%s\n' "$password" | chpasswd
password_changed=1
unset password
systemctl reload ssh
log 'Concluido: DNS configurado; senha do root definida; SSH recarregado.'
log "Backup das configuracoes: $backup/original"
log 'Mantenha esta sessao aberta e teste outro login SSH como root.'
log 'Para conferir o DNS: resolvectl status (ou systemd-resolve --status).'
