reiniciar_nodo.yml

# Reinicia un nodo sin cortar servicio: lo saca del clúster (drain), lo reinicia,
# espera a que vuelva Ready y lo devuelve (uncordon). Si algo falla, lo devuelve
# igualmente y para todo el playbook.
- name: El master-1 no se reinicia solo
  ansible.builtin.debug:
    msg: "⚠ {{ inventory_hostname }} pide reinicio, pero desde aquí corre el portal: reinícialo a mano cuando te venga bien."
  when: controlador | default(false)

- name: Reiniciar {{ inventory_hostname }}
  when: not (controlador | default(false))
  block:
    # Un master que vuelve con la red inestable puede hacer saltar la VIP (keepalived)
    # o dejar etcd sin quórum. Es la comprobación de docs/operacion.md, con TCP en vez
    # de ping porque el contenedor no tiene capacidades (NET_RAW).
    - name: Red estable hasta el master (30 conexiones a la API, ninguna puede fallar)
      ansible.builtin.command:
        argv:
          - python3
          - -c
          - |
            import socket, sys, time
            fallos = 0
            for _ in range(30):
                try:
                    socket.create_connection((sys.argv[1], 6443), timeout=2).close()
                except OSError:
                    fallos += 1
                time.sleep(0.5)
            print(f"{fallos} de 30 conexiones fallidas")
            sys.exit(1 if fallos else 0)
          - "{{ ansible_host }}"
      changed_when: false
      delegate_to: localhost
      when: inventory_hostname in groups['masters']

    - name: Volúmenes local-path de este nodo (sus servicios se paran hasta que vuelva)
      ansible.builtin.shell: |
        set -o pipefail
        kubectl --kubeconfig {{ kubeconfig }} get pv -o json | python3 -c '
        import json, sys
        for v in json.load(sys.stdin)["items"]:
            s = v["spec"]
            if (s.get("storageClassName") != "local-path" or not s.get("claimRef")
                    or v["status"].get("phase") != "Bound"):  # los Released ya no dan servicio
                continue
            terms = s.get("nodeAffinity", {}).get("required", {}).get("nodeSelectorTerms", [])
            nodos = [x for t in terms for e in t.get("matchExpressions", []) for x in e.get("values", [])]
            if sys.argv[1] in nodos:
                print(s["claimRef"]["namespace"] + "/" + s["claimRef"]["name"])
        ' {{ inventory_hostname | quote }}
      args:
        executable: /bin/bash
      register: local_path
      changed_when: false
      delegate_to: localhost

    - name: Aviso de corte
      ansible.builtin.debug:
        msg: "⚠ Sin servicio hasta que {{ inventory_hostname }} vuelva: {{ local_path.stdout_lines | join(', ') }}"
      when: local_path.stdout_lines | length > 0

    - name: Sacar el nodo del clúster (drain)
      ansible.builtin.command: >-
        kubectl --kubeconfig {{ kubeconfig }} drain {{ inventory_hostname }}
        --ignore-daemonsets --delete-emptydir-data --timeout=10m
      delegate_to: localhost
      changed_when: true

    - name: Reiniciar
      ansible.builtin.command: sudo -n /usr/local/sbin/nodo-mantenimiento reiniciar
      changed_when: true

    - name: Esperar a que se apague
      ansible.builtin.pause:
        seconds: 40

    - name: Esperar a que vuelva por SSH
      ansible.builtin.wait_for_connection:
        timeout: 900
        sleep: 10

    - name: Esperar a que el nodo esté Ready
      ansible.builtin.command: >-
        kubectl --kubeconfig {{ kubeconfig }} wait --for=condition=Ready
        node/{{ inventory_hostname }} --timeout=10m
      delegate_to: localhost
      changed_when: false
  always:
    - name: Devolver el nodo al clúster (uncordon)
      ansible.builtin.command: kubectl --kubeconfig {{ kubeconfig }} uncordon {{ inventory_hostname }}
      delegate_to: localhost
      changed_when: true