Le projet

L’idée de départ était simple : déployer OCS Inventory NG pour centraliser l’inventaire matériel et logiciel de l’infrastructure. Un serveur, des agents, une console web. Rien de bien compliqué.

Spoiler : Je coyais que ce serait du caviar, ben j’ai mangé des cailloux.


Acte 1 — L’installation serveur, ou “perl est ton ennemi”

L’installation d’OCS côté serveur passe par un script setup.sh qui pose des questions. Beaucoup de questions. Et qui attend des réponses précises — appuyer sur Entrée sans réfléchir, c’est la croix et la bannière.

Après avoir répondu correctement à l’interrogatoire, premier redémarrage d’Apache. Échec.

Can't locate Switch.pm in @INC
Can't load Perl module Apache::Ocsinventory

Deux modules Perl absents. libswitch-perl, ça se trouve facilement. libxml-entities-perl… introuvable dans les dépôts Ubuntu 24.04. Direction cpan, configuration automatique, téléchargement depuis internet, compilation, installation. Pour un module. Bienvenue en 2026.

Une fois les modules en place, Apache repart. Victoire ? Pas encore.


Acte 2 — La base de données qui ne répond pas

Une fois Apache relancé, on accède à /ocsreports pour la première configuration. On tombe sur ce formulaire — noter au passage le warning en rouge qui annonce déjà la couleur sur z-ocsinventory-server.conf :

Formulaire de configuration de la base de données OCS

Je saisis les infos de la base de données. Internal Server Error.

Fouille des logs Apache. Le module Perl ne peut pas se connecter à MariaDB. Inspection du fichier de configuration z-ocsinventory-server.conf et….. damned:

PerlSetVar OCS_DB_PWD ocs

Le mot de passe par défaut était resté en place. Le script d’installation ne l’avait pas mis à jour malgré les réponses. Correction à la main, redémarrage d’Apache. Cette fois c’est bon.

À retenir : après l’installation, vérifiez systématiquement que le mot de passe dans z-ocsinventory-server.conf correspond bien à ce que vous avez saisi.

Cette fois le formulaire passe et on obtient enfin la confirmation :

Installation terminée

En cliquant sur le lien, surprise — un écran de mise à jour du schéma de base de données. Beaucoup de gens paniquent en le voyant, c’est pourtant tout à fait normal :

Mise à jour du schéma de base de données

Un clic sur “Perform the update” et on accède à l’interface. Première chose qu’on voit :

Tableau de bord OCS avec alertes sécurité

Deux alertes sécurité immédiates : supprimer install.php et changer le mot de passe admin par défaut. Une fois traité, on attaque le déploiement des agents Windows.


Acte 3 — L’agent Windows, ou “le packager qui ne package pas”

Côté client Windows, OCS propose un outil appelé OCS Packager (qui s’appelle en réalité Packager-for-Windows sur GitHub, détail amusant). L’idée est séduisante : on lui donne l’installeur, le certificat CA, les paramètres de connexion, et il génère un exécutable tout-en-un prêt à être déployé via GPO.

En théorie.

Interface OCS Packager

En pratique, l’exécutable généré ignorait superbement les paramètres configurés et continuait à pointer vers http://ocsinventory-ng/ocsinventory — l’adresse par défaut codée en dur. Relance du Packager, vérification des options, même résultat.

Abandon du Packager et choix d’une approche plus directe : un batch GPO qui installe l’agent, écrase le fichier ocsinventory.ini avec la bonne configuration, et copie le certificat CA au bon endroit.

@echo off
if exist "C:\Program Files\OCS Inventory Agent\OCSInventory.exe" goto config

\\serveur\deploy$\OCS\OCS-Windows-Agent-Setup-x64.exe /S /NOTRAY
timeout /t 10

:config
net stop "OCS Inventory Service"
copy /Y \\serveur\deploy$\OCS\ocsinventory.ini "C:\ProgramData\OCS Inventory NG\Agent\ocsinventory.ini"
copy /Y \\serveur\deploy$\OCS\ca.cert.pem "C:\ProgramData\OCS Inventory NG\Agent\cacert.pem"
net start "OCS Inventory Service"

:fin
exit

Rustique. Efficace.


Acte 4 — Le cacert.pem, ou “le détail qui tue”

L’agent installé, la configuration correcte en place… et toujours rien qui remonte dans la console.

Après investigation, le fichier cacert.pem n’était tout simplement pas présent dans le répertoire de l’agent. Sans ce fichier, l’agent refuse la connexion HTTPS — logique — mais sans le dire clairement.

Une fois le certificat copié manuellement, la machine est apparue dans la console en quelques secondes.

C’est exactement pour ça que le batch copie désormais les deux fichiers : l’ocsinventory.ini et le cacert.pem.


Acte 5 — Les serveurs Linux, ou “merci Ansible”

Pour les serveurs Linux, pas question de se battre avec des GPO et des batch. Il y a déjà un playbook Ansible qui tourne deux fois par semaine pour mettre à jour tous les serveurs — NTP, paquets, rapport HTML par mail. J’ai simplement ajouté les tâches OCS dedans.

L’idée : installer le paquet ocsinventory-agent, déposer le certificat CA, écraser la configuration avec les bons paramètres, et lancer un inventaire immédiat si c’est une nouvelle installation. Le tout intégré dans le flux existant, avec un statut “installé / déjà présent” dans le rapport HTML.

Avant de lancer le playbook, il faut copier le certificat CA sur le serveur Ansible :

mkdir -p /etc/ansible/files
scp user@serveur-ocs:/etc/ssl/ocs/ca.cert.pem /etc/ansible/files/

Voici le playbook complet avec les tâches OCS intégrées :

---
- name: Configuration du serveur NTP, fuseau horaire et mise à jour des paquets
  hosts: serveurs
  become: true
  become_user: root

  tasks:
    # --- Définir le fuseau horaire ---
    - name: Définir le fuseau horaire sur Europe/Paris
      community.general.timezone:
        name: Europe/Paris
      register: timezone_set
      when: inventory_hostname not in ['kronoss', 'demeter']

    # --- Gestion de Chrony ---
    - name: S'assurer que Chrony est installé
      ansible.builtin.apt:
        name: chrony
        state: present
      register: chrony_installed
      when: inventory_hostname != 'kronoss'

    - name: Déployer la configuration chrony.conf
      ansible.builtin.copy:
        dest: /etc/chrony/chrony.conf
        content: |
          server 192.168.x.x iburst prefer
          server 192.168.x.x iburst
          keyfile /etc/chrony/chrony.keys
          driftfile /var/lib/chrony/chrony.drift
          ntsdumpdir /var/lib/chrony
          logdir /var/log/chrony
          maxupdateskew 100.0
          rtcsync
          makestep 1 3
          leapsectz right/UTC          
        backup: yes
      register: chrony_config_changed
      when: inventory_hostname not in ['kronoss', 'demeter']

    - name: Redémarrer le service Chrony
      ansible.builtin.systemd:
        name: chronyd
        state: restarted
        enabled: yes
      register: chrony_service_restarted
      when: inventory_hostname not in ['kronoss', 'demeter']

    - name: Vérifier la synchronisation NTP
      ansible.builtin.command:
        cmd: chronyc tracking
      register: ntp_status
      changed_when: false

    # --- Installation agent OCS Inventory ---
    - name: Installer l'agent OCS Inventory
      ansible.builtin.apt:
        name: ocsinventory-agent
        state: present
      register: ocs_installed

    - name: Créer le répertoire de configuration OCS
      ansible.builtin.file:
        path: /etc/ocsinventory
        state: directory
        mode: '0755'

    - name: Copier le certificat CA pour OCS
      ansible.builtin.copy:
        src: /etc/ansible/files/ca.cert.pem
        dest: /etc/ocsinventory/cacert.pem
        mode: '0644'

    - name: Déployer la configuration OCS
      ansible.builtin.copy:
        dest: /etc/ocsinventory/ocsinventory-agent.cfg
        content: |
          server=https://serveur-ocs.domaine.local/ocsinventory
          ssl=1
          ca=/etc/ocsinventory/cacert.pem
          logfile=/var/log/ocsinventory-agent/ocsinventory-agent.log
          debug=0          
        backup: yes

    - name: Lancer un inventaire immédiat si nouvel install
      ansible.builtin.command:
        cmd: ocsinventory-agent --force
      changed_when: false
      when: ocs_installed.changed

    # --- Mise à jour des paquets ---
    - name: Mettre à jour la liste des paquets
      ansible.builtin.apt:
        update_cache: yes
        cache_valid_time: 3600
      register: apt_update_result

    - name: Effectuer une mise à jour complète des paquets
      ansible.builtin.apt:
        upgrade: full
        autoremove: yes
        autoclean: yes
      register: apt_upgrade_result

    # --- Gestion du redémarrage (informations uniquement) ---
    - name: Vérifier si un redémarrage est nécessaire
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_required

    # --- Vérification espace disque (seuil 70%) ---
    - name: Vérifier espace disque sur chaque serveur
      ansible.builtin.shell: >
        df -h --output=source,pcent,target | tail -n +2 |
        grep -v tmpfs | grep -v udev |
        awk -F'[% ]+' '$2 >= 70 {print $1"|"$2"|"$3}'        
      register: disk_check
      changed_when: false

    # --- Vérification certificats SSL (seuil 30 jours) ---
    - name: Vérifier expiration certificats SSL
      ansible.builtin.shell: |
        for cert in /etc/letsencrypt/live/*/cert.pem; do
          domain=$(echo $cert | cut -d'/' -f5)
          expiry=$(openssl x509 -enddate -noout -in $cert | cut -d'=' -f2)
          expiry_epoch=$(date -d "$expiry" +%s)
          now_epoch=$(date +%s)
          days_left=$(( ($expiry_epoch - $now_epoch) / 86400 ))
          echo "$domain|$days_left"
        done        
      register: ssl_check
      changed_when: false
      delegate_to: serveur-web
      run_once: true

    # --- Construction du message récapitulatif global en HTML ---
    - name: Construire le message récapitulatif global
      ansible.builtin.set_fact:
        recap_message: |
          <html>
          <body style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
          <div style="max-width: 800px; margin: auto; background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
            <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
              🖥️ Rapport de mise à jour Ansible
            </h2>
            <p style="color: #7f8c8d;">Exécuté le {{ ansible_date_time.date }} à {{ ansible_date_time.time }}</p>

          {% for host in ansible_play_hosts %}
            <div style="margin-top: 20px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;">
              <div style="background-color: #3498db; color: white; padding: 10px 15px;">
                <h3 style="margin: 0;">{{ host | upper }}</h3>
              </div>
              <table style="width: 100%; border-collapse: collapse;">
                <tr style="background-color: #f9f9f9;">
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee; width: 60%;">Fuseau horaire (Europe/Paris)</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].timezone_set.changed %}
                      <span style="color: #e67e22;">⚠️ MODIFIÉ</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ OK</span>
                    {% endif %}
                  </td>
                </tr>
                <tr>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Chrony</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].chrony_installed.changed %}
                      <span style="color: #e67e22;">⚠️ INSTALLÉ</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ PRÉSENT</span>
                    {% endif %}
                  </td>
                </tr>
                <tr style="background-color: #f9f9f9;">
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Configuration NTP</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].chrony_config_changed.changed %}
                      <span style="color: #e67e22;">⚠️ MODIFIÉ</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ OK</span>
                    {% endif %}
                  </td>
                </tr>
                <tr>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Agent OCS Inventory</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].ocs_installed.changed %}
                      <span style="color: #3498db;">🔄 INSTALLÉ</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ PRÉSENT</span>
                    {% endif %}
                  </td>
                </tr>
                <tr style="background-color: #f9f9f9;">
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Mise à jour des paquets</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].apt_upgrade_result.changed %}
                      <span style="color: #3498db;">🔄 EFFECTUÉE</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ À JOUR</span>
                    {% endif %}
                  </td>
                </tr>
                <tr style="background-color: #f9f9f9;">
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Redémarrage nécessaire</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                    {% if hostvars[host].reboot_required.stat.exists %}
                      <span style="color: #e74c3c;">🔴 OUI</span>
                    {% else %}
                      <span style="color: #27ae60;">✅ NON</span>
                    {% endif %}
                  </td>
                </tr>
                <tr>
                  <td style="padding: 8px 15px;">Espace disque (&gt;70%)</td>
                  <td style="padding: 8px 15px;">
                    {% set disk_issues = [] %}
                    {% for line in hostvars[host].disk_check.stdout_lines %}
                      {% set parts = line.split('|') %}
                      {% if parts | length == 3 %}
                        {% set _ = disk_issues.append(parts) %}
                      {% endif %}
                    {% endfor %}
                    {% if disk_issues | length > 0 %}
                      {% for d in disk_issues %}
                        <span style="color: #e74c3c;">🔴 {{ d[0] }} → {{ d[1] }}% ({{ d[2] }})</span><br>
                      {% endfor %}
                    {% else %}
                      <span style="color: #27ae60;">✅ OK</span>
                    {% endif %}
                  </td>
                </tr>
              </table>
            </div>
          {% endfor %}

            <!-- Section SSL -->
            <div style="margin-top: 30px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;">
              <div style="background-color: #8e44ad; color: white; padding: 10px 15px;">
                <h3 style="margin: 0;">🔒 Certificats SSL</h3>
              </div>
              <table style="width: 100%; border-collapse: collapse;">
                <tr style="background-color: #f0f0f0; font-weight: bold;">
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee; width: 60%;">Domaine</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Jours restants</td>
                </tr>
                {% for line in ssl_check.stdout_lines %}
                  {% set parts = line.split('|') %}
                  {% if parts | length == 2 %}
                    {% set days = parts[1] | int %}
                    <tr style="background-color: {% if days <= 30 %}#fdf2f2{% else %}#f9f9f9{% endif %};">
                      <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">{{ parts[0] }}</td>
                      <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
                        {% if days <= 30 %}
                          <span style="color: #e74c3c; font-weight: bold;">🔴 {{ days }} jours — RENOUVELLEMENT URGENT</span>
                        {% elif days <= 60 %}
                          <span style="color: #e67e22;">⚠️ {{ days }} jours</span>
                        {% else %}
                          <span style="color: #27ae60;">✅ {{ days }} jours</span>
                        {% endif %}
                      </td>
                    </tr>
                  {% endif %}
                {% endfor %}
              </table>
            </div>

            <p style="color: #95a5a6; font-size: 12px; margin-top: 20px; text-align: center;">
              Généré automatiquement par Ansible
            </p>
          </div>
          </body>
          </html>          
      run_once: true

    # --- Envoi d'un seul e-mail récapitulatif (en HTML) ---
    - name: Envoyer un e-mail récapitulatif unique
      ansible.builtin.mail:
        host: serveur-mail.domaine.local
        port: 25
        to: "admin@domaine.local"
        subject: "Rapport de mise à jour Ansible - {{ ansible_date_time.date }}"
        body: "{{ recap_message }}"
        from: "ansible@serveur-ansible.domaine.local"
        secure: never
        subtype: html
      run_once: true
      delegate_to: localhost

Le résultat : au prochain passage du cron, tous les serveurs Linux installent l’agent, remontent leur inventaire, et le rapport mail indique proprement “INSTALLÉ” ou “PRÉSENT” pour chacun.


Ce qui fonctionne à la fin

  • Serveur OCS en HTTPS avec certificat signé par la CA interne
  • Agents Windows déployés via GPO avec configuration automatique
  • Agents Linux déployés via Ansible
  • Toutes les machines remontent leur inventaire complet : CPU, RAM, disques, logiciels, IP, MAC

Le résultat final est propre. Le chemin pour y arriver l’était beaucoup moins.


Leçons retenues

  • Vérifier le mot de passe dans z-ocsinventory-server.conf après installation — le mot de passe y est en clair, restriction des permissions immédiatement après :
    sudo chmod 640 /etc/apache2/conf-available/z-ocsinventory-server.conf
    sudo chown root:www-data /etc/apache2/conf-available/z-ocsinventory-server.conf
    
  • Ne pas faire confiance au Packager pour propager les paramètres — écraser le .ini directement
  • Toujours copier le cacert.pem dans le répertoire de l’agent en HTTPS
  • XML::Entities n’est pas dans les dépôts Ubuntu 24.04 — prévoir cpan pour l’installer