Journal Editer en parallèle les paramètres BIOS de plusieurs machines grâce à tmux

Posté par  . Licence CC By‑SA.
Étiquettes :
28
3
août
2023

Bonjour,

en ce moment, j'ai besoin de mettre à jour les paramètres BIOS de beaucoup de machines. Heureusement, je peux interagir avec elles à distance grâce à ipmitool.

Dans le journal précédent, j'ai écrit un outil pour facilement configurer des machines à distance pour qu'elles redémarrent dans les paramètres BIOS.

Maintenant, il reste qu'il faut que j'édite les paramètres de toutes ces machines. Toutes ces machines sont identiques mais je ne peux pas changer les paramètres de BIOS à distance par ipmi. Je peux disposer, avec ipmitool, d'une console déportée, mais il me faudrait quand même répéter toujours la même manipulation. Ce n'est pas très satisfaisant.

Le script tmux en un clin d'oeil

En cherchant un peu, j'ai vu qu'on pouvait, avec tmux, scripter la création de plusieurs panneaux (pane) et surtout les synchroniser de telle sorte que les saisies clavier soient soumises en parallèle à tout les fenêtres.

Pour le coup, quelques lignes de script bash. Je suis parti de ce gist comme point de départ puis d'un peu plusieurs liens/forums.

#!/bin/bash

SESSIONNAME="bios"
BASE=$1
NB=$2

# Check if the session has already been created
tmux has-session -t $SESSIONNAME 

if [ $? != 0 ]
then
        # Create the session
        tmux new-session -s $SESSIONNAME -n "BIOS edition" -d

        # Access the ipmi console for the first pane
        tmux send-keys -t $SESSIONNAME "ipmitool -I lanplus -H ikyle$BASE -U root -P XXXXXX sol activate" C-m

        for (( i=1; i<=$(expr $NB - 1); i++ )); do
                # Compute the node name 
                nodenum=$(expr $i + $BASE)
                # We split the first pane
                tmux split-window -t $SESSIONNAME  
                # Start the ipmi console
                tmux send-keys -t $SESSIONNAME "ipmitool -I lanplus -H ikyle$nodenum -U root -P XXXXXX sol activate" C-m
                # Reset the layout to tiled
                tmux select-layout -t $SESSIONNAME tiled
        done

        # Synchronize all the panes so that the keystrokes are sent
        # simultaneously to all the panes
        tmux setw -t $SESSIONNAME synchronize-panes on
fi

tmux attach -t $SESSIONNAME

qu'on appelle ensuite par

~:$ ./tmux.sh 30 10

qui me lance la console sur 10 machines de Kyle30 à Kyle39. Et je peux alors éditer en parallèle tout les bios.

Edition en parallèle des BIOS de plusieurs machines

En détail

La première partie permet de se vérifier si la session existe déjà. Si oui, on s'y reconnecte, sinon on la crée.

SESSIONNAME="bios"

# Check if the session has already been created
tmux has-session -t $SESSIONNAME 

if [ $? != 0 ]
then

...
fi
tmux attach -t $SESSIONNAME

Si on crée la session, on la crée en mode détaché, avec un petit titre et son nom $SESSIONNAME :

# Create the session
tmux new-session -s $SESSIONNAME -n "BIOS edition" -d

Ensuite, je vais créer une succession de division et chaque fois, envoyer ma commande de connexion à la console dans le dernier panneau créé. Comme je vais perdre la référence au premier panneau, j'injecte ma commande de connexion à la console avant de me lancer dans les divisions.

# Access the ipmi console for the first pane
tmux send-keys -t $SESSIONNAME "ipmitool -I lanplus -H ikyle$BASE -U root -P XXXXXX sol activate" C-m

Ici on soumet la commande shell ipmitool qu'on valide avec le retour charriot C-m (qu'on aurait pu remplacer par Enter.

Vient ensuite la boucle pour créer tout les panneaux et me connecter aux consoles avec deux nouvelles commandes tmux split-window -t $SESSIONNAME pour diviser le panneau $SESSIONNAME et tmux select-layout -t $SESSIONNAME tiled qui permet de conserver une division régulière horizontale et verticale. Tmux limite le nombre de division verticale ou horizontale. Je peux disposer de plus de consoles avec cette disposition.

  • # Jalousie

    Posté par  (site web personnel) . Évalué à 3.

    Je bricole un cluster de mini-PC sans IPMI, après des tonnes de Ctrl-Alt-Suppr et de rebranchements de claviers/souris, je tiens à dire que je crève de jalousie.

    • [^] # Re: Jalousie

      Posté par  . Évalué à 2.

      En fait, on a aussi une collection de machines qui, malheureusement, n'ont pas l'ipmi .

      Pour ces machines, on a un boitier KVM (Clavier, Souris, Ecran) et un collègue me disait qu'il existe des KVMs avec accès à distance. Mais je ne sais pas à quel point on peut y accéder en ligne de commande, je n'ai pas encore testé.

      Combiné peut être avec des "pdsh reboot" … peut être que ça peut être mieux que rien .

      Github: https://github.com/jeremyfix

      • [^] # Re: Jalousie

        Posté par  . Évalué à 3.

        Sinon sur les machines en UEFI y'a pas mal de choses exposées dans les variables, voire selon l'humeur du fabricant de manière directement exploitable comme dans le menu du firmware.
        Exemple sur un thinkpad P14s, où j'ai voulu changer l'attribution de VRAM:

        root@monPC:/sys/class/firmware-attributes/thinklmi# cat attributes/UMAFramebufferSize/current_value 
        1GB
        root@monPC:/sys/class/firmware-attributes/thinklmi# cat attributes/UMAFramebufferSize/display_name 
        UMAFramebufferSize
        root@monPC:/sys/class/firmware-attributes/thinklmi# cat attributes/UMAFramebufferSize/possible_values 
        Auto;512MB;1GB;2GB
        
  • # Hou, sympa !

    Posté par  (site web personnel) . Évalué à 3.

    Je fais ça avec cssh (ClusterSSH), et c'est pas toujours pratique (le tunnel X est pas toujours dispo).

    Merci.

    Proverbe Alien : Sauvez la terre ? Mangez des humains !

  • # alternative

    Posté par  (site web personnel) . Évalué à 4.

    J'ai écrit ça il y a quelques temps qui fait un truc similaire pour les gens qui ont pas tmux (comme c'était le cas de tout le monde en 2006) : http://cretonnerre.krunch.be/~krunch/src/tcpmux.c

    // A select()-based TCP multiplexer. It will accept connections
    // on the specified port and send its stdin to connected clients.
    // This has nothing to do with the tcpmux service.
    // 
    // $ cc -Wall -W -pedantic -std=c99 -o tcpmux tcpmux.c
    // 
    // TODO: select() sucks, use something better
    // TODO: turn this into a netcat patch
    // TODO: _XOPEN_SOURCE should be in Makefile
    // TODO: RESEND should be a command line option
    // TODO: are all error conditions checked ?
    //
    // Copyright (c) 2006 Adrien Kunysz
    //
    // Permission is hereby granted, free of charge, to any person obtaining a
    // copy of this software and associated documentation files (the "Software"),
    // to deal in the Software without restriction, including without limitation
    // the rights to use, copy, modify, merge, publish, distribute, sublicense,
    // and/or sell copies of the Software, and to permit persons to whom the
    // Software is furnished to do so, subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in
    // all copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    // DEALINGS IN THE SOFTWARE.
    
    #define _XOPEN_SOURCE  500
    //#define RESEND  // data sent by clients are resent to other clients
    //#define NDEBUG  // less verbosity
    
    #include <stdio.h>    // stderr, fprintf()
    #include <stdlib.h>   // exit(), EXIT_*, atoi(), NULL
    #include <string.h>   // memset()
    #include <errno.h>    // errno
    
    #include <sys/types.h>   // size_t, ssize_t
    #include <sys/socket.h>  // socket(), bind(), listen(), accept(), send(),...
    #include <sys/time.h>    // select(), FD_*, fd_set
    #include <arpa/inet.h>   // htons()
    #include <netinet/in.h>  // INADDR_ANY, sockaddr_in
    #include <unistd.h>      // STDIN_FILENO, STDERR_FILENO, close(), read()
    #include <fcntl.h>       // fcntl(), F_SETFL, O_NONBLOCK
    
    #define MAX_UNACCEPTED_CONNS  5  // listen()'s second argument
    #define MAX_CLIENTS  32
    #define BUFSZ  1024
    
    #if MAX_CLIENTS + 2 > FD_SETSIZE
    #  error MAX_CLIENTS is too high
    #endif
    
    #ifdef NDEBUG
    #  define debug(...)
    #  define wdebug(buf, len)
    #else
    #  define debug(...)  fprintf(stderr, __VA_ARGS__)
    #  define wdebug(buf, len)  write(STDERR_FILENO, buf, len)
    #endif
    
    #define err(...)  do {                                   \
                              fprintf(stderr, __VA_ARGS__);  \
                              exit(EXIT_FAILURE);            \
                      } while (0)
    
    int setup_socket(int portnum)
    {
        int res = -1;
        if (-1 == (res = socket(AF_INET, SOCK_STREAM, 0)))
            err("Unable to create socket.\n");
        int yes = 1;
        if (-1 == setsockopt(res, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)))
            fprintf(stderr, "Unable to set SO_REUSEADDR option.\n");
        struct sockaddr_in myaddr = {
            .sin_family = AF_INET,
            .sin_addr.s_addr = INADDR_ANY,
            .sin_port = htons(portnum)
        };
        memset(&(myaddr.sin_zero), '\0', 8);
        if (-1 == bind(res, (struct sockaddr *)&myaddr, sizeof(myaddr)))
            err("Unable to bind address to socket.\n");
        if (-1 == listen(res, MAX_UNACCEPTED_CONNS))
            err("Unable to listen on socket.\n");
        return res;
    }
    
    int accept_client(int serverfd)
    {
        struct sockaddr_in clientaddr;
        socklen_t addrlen = sizeof(clientaddr);
        int res = accept(serverfd, (struct sockaddr *)&clientaddr, &addrlen);
        if (-1 == res)
            err("accept() failure\n");
        if (-1 == fcntl(res, F_SETFL, O_NONBLOCK))
            fprintf(stderr, "Unable to set non blocking mode on fd.\n");
        return res;
    }
    
    size_t sendall(int fd, char *buf, size_t len)
    {
        size_t sent = 0;
        while (sent < len) {
            errno = 0;
            ssize_t this_block = send(fd, &buf[sent], len - sent, 0);
            if (0 > this_block) {
                if (EAGAIN == errno || EWOULDBLOCK == errno)
                    fprintf(stderr, "Output buffer is full ?\n");
                break;
            }
            sent += (size_t)this_block;
        }
        return sent;
    }
    
    int main(int argc, char **argv)
    {
        if (2 != argc)
            err("Usage: %s PORT\n", argv[0]);
        const int port = atoi(argv[1]);
    
        const int inputfd = STDIN_FILENO;
        const int serverfd = setup_socket(port);
        static int clientsfds[MAX_CLIENTS];
        for (unsigned int i = 0; i < MAX_CLIENTS; i++)
            clientsfds[i] = -1;
    
        static fd_set allfds;
        FD_ZERO(&allfds);
        FD_SET(serverfd, &allfds);
        FD_SET(inputfd, &allfds);
    
        while (1) {
            static fd_set selectfds;
            selectfds = allfds;
            if (-1 == select(FD_SETSIZE, &selectfds, NULL, NULL, NULL))
                err("select() failure\n");
    
            if (FD_ISSET(serverfd, &selectfds)) {  // client connection
                debug("Got new client.\n");
                unsigned int fdidx;
                for (fdidx = 1; fdidx < MAX_CLIENTS; fdidx++)
                    if (-1 == clientsfds[fdidx])
                        break;
                if (MAX_CLIENTS+1 == fdidx)
                    fprintf(stderr, "Too many clients.\n");
                clientsfds[fdidx] = accept_client(serverfd);
    
                FD_SET(clientsfds[fdidx], &allfds);
                continue;
            }
    
            static char buf[BUFSZ];
            if (FD_ISSET(inputfd, &selectfds)) {  // input is readable
                ssize_t len = read(inputfd, buf, sizeof(buf));
                if (0 > len)
                    err("mysterious read failure\n");
                if (0 == len)  // EOF
                    exit(EXIT_SUCCESS);
                size_t ulen = (size_t)len;
                for (unsigned int i = 0; i < MAX_CLIENTS; i++) {
                    if (-1 == clientsfds[i])
                        continue;
                    if (ulen != sendall(clientsfds[i], buf, ulen))
                        fprintf(stderr, "send() failure\n");
                }
                continue;
            }
    
            // a client sent something
            unsigned int fdidx;
            for (fdidx = 0; fdidx < MAX_CLIENTS; fdidx++) {
                if (clientsfds[fdidx] == -1)
                    continue;
                if (FD_ISSET(clientsfds[fdidx], &selectfds))
                    break;
            }
            if (MAX_CLIENTS == fdidx) {  // wtf ? no readable client found
                fprintf(stderr, "select() lied ?\n");
                continue;
            }
            ssize_t len = read(clientsfds[fdidx], buf, sizeof(buf));
            if (0 > len)
                fprintf(stderr, "failed to read from client\n");
            if (0 == len) {  // client closed connection
                FD_CLR(clientsfds[fdidx], &allfds);
                close(clientsfds[fdidx]);
                clientsfds[fdidx] = -1;
                debug("Client disconnected.\n");
                continue;
            }
            wdebug(buf, (size_t)len);
            #ifdef RESEND
            for (unsigned int i = 0; i < MAX_CLIENTS; i++)
                if (-1 != clientsfds[i] && i != fdidx)
                    sendall(clientsfds[i], buf, (size_t)len);
            #endif
        }
    }

    pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

  • # pour des trucs plus sporadiques...

    Posté par  (Mastodon) . Évalué à 3.

    …c'est bien de mapper un raccourci clavier pour synchroniser les panes dans son tmuxrc.

  • # tmuxinator / synchronize-panes

    Posté par  (site web personnel) . Évalué à 2. Dernière modification le 06 août 2023 à 12:32.

    Pas mal, même si je préfère utiliser Ansible pour ce genre de choses.

    Deux questions, toutefois.

    Il y a tmuxinator, qui permet de créer des profils tmux sympas. As-tu pensé à l'utiliser ?

    Il y a la fonction synchronize-panes de tmux, du coup, même question ?

    • [^] # Re: tmuxinator / synchronize-panes

      Posté par  (Mastodon) . Évalué à 3.

      Si tu avais lu son script tu te serais rendu compte qu'il s'appuie sur la cette fonction. Son script ajout juste toute la mécanique de création des panes et connection automatiques aux différents noeuds via ipmitool.

    • [^] # Re: tmuxinator / synchronize-panes

      Posté par  . Évalué à 1.

      Pas mal, même si je préfère utiliser Ansible pour ce genre de choses.

      Par contre, je ne suis pas sûr de voir comment tu le ferai avec ansible ?

      Il y a tmuxinator, qui permet de créer des profils tmux sympas. As-tu pensé à l'utiliser ?

      Je ne connaissais pas tmuxinator. Sauf erreur de ma part, ça m'obligerait à créer un fichier de config pour chaque session multi-noeuds. Même créé à la volée, ça me paraît inutilement lourd.

      Il y a la fonction synchronize-panes de tmux, du coup, même question ?

      synchronize-panes est en effet la fonction que j'utilise pour que les panneaux soient synchronisés;

      D'ailleurs, à l'usage, je préfère l'exécuter manuellement après coup. Il arrive en effet parfois que la session IPMI ne s'établisse pas correctement et j'ai besoin de la relancer pour certains nœuds.

      Github: https://github.com/jeremyfix

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.