Aller au contenu

Programmes

add-dhcp-client.py

#!/usr/bin/env python3

import sys
from validation import if_ipv4valide, if_macvalide  
from dhcp import dhcp_add, ip_other_mac_exists, mac_exists  
from config import get_dhcp_server, load_config  
from fabric import Connection
from paramiko.ssh_exception import NoValidConnectionsError, SSHException

def main():
    # vérifie qu'il y a exactement 2 arguments : mac et ip
    if len(sys.argv) != 3:
        print("usage: add-dhcp-client.py MAC IP")
        sys.exit(1)

    mac = sys.argv[1]
    ip = sys.argv[2]

    # valide l'ip, sinon quitte avec message d'erreur
    if not if_ipv4valide(ip):
        print("bad ip address.")
        sys.exit(1)

    # valide le mac, sinon quitte avec message d'erreur
    if not if_macvalide(mac):
        print("bad mac address.")
        sys.exit(1)

    # charge la config depuis le fichier yaml
    config = load_config('config_superviseur.yaml', True)
    # cherche quel serveur gère cette ip
    dhcp_server = get_dhcp_server(ip, config)

    if dhcp_server is None:
        print("unable to identify dhcp server")
        sys.exit(1)

    # récupère l'adresse ip du serveur
    dhcp_server_addr = list(dhcp_server.keys())[0]
    cnx = Connection(host=dhcp_server_addr, user='dhcp-mod')

    try:
        # si le mac n'existe pas déjà
        if not mac_exists(cnx, mac, config):
            # et si l'ip n'est pas déjà utilisée par un autre mac
            if not ip_other_mac_exists(cnx, ip, mac, config):
                dhcp_add(ip, mac, dhcp_server_addr, config)  # ajoute la ligne
            else:
                print("ip address already in use.")
                sys.exit(1)
        else:
            # si le mac existe, mais qu'on veut changer l'ip
            if not ip_other_mac_exists(cnx, ip, mac, config):
                dhcp_add(ip, mac, dhcp_server_addr, config)  # met à jour
            else:
                print("ip address already in use.")
                sys.exit(1)

    except NoValidConnectionsError:
        print('pas de connexion valide.')
    except SSHException as e:
        print(f'erreur de connexion ssh : {e}')


if __name__ == '__main__':
    main()

check-dhcp.py

#!/usr/bin/env python3

import sys
from dhcp import dhcp_list  
from config import get_dhcp_server, load_config
from fabric import Connection
from paramiko.ssh_exception import NoValidConnectionsError, SSHException

def main():
    # charge la config depuis le yaml
    config = load_config('config_superviseur.yaml', True)

    # si plus d'un paramètre en ligne de commande -> erreur
    if len(sys.argv) > 2:
        print("error: too much parameters")
        sys.exit(1)

    # si on a un paramètre (réseau ou ip de serveur)
    if len(sys.argv) == 2:
        param = sys.argv[1]

        # si le paramètre contient '/', c'est un réseau
        if '/' in param:
            found = False
            # on cherche le serveur qui gère ce réseau
            for server, network in config.get("dhcp-servers", {}).items():
                if network == param:
                    server_ip = server
                    found = True
                    break
            if not found:
                print("cannot identify dhcp server")
                sys.exit(1)
        else:
            # sinon c'est une ip serveur -> vérifie avec get_dhcp_server
            dhcp_server = get_dhcp_server(param, config)
            if not dhcp_server:
                print("cannot identify dhcp server")
                sys.exit(1)
            server_ip = list(dhcp_server.keys())[0]

        try:
            # récupère la liste des associations sur le serveur ciblé
            list_dhcp = dhcp_list(server_ip, config)
            mac_tot = {}
            ip_tot = {}

            # construit deux dictionnaires : un pour mac -> lignes, un pour ip -> lignes
            for addr in list_dhcp:
                mac = addr["mac"]
                ip = addr["ip"]
                line = f"dhcp-host={mac},{ip}"

                if mac not in mac_tot:
                    mac_tot[mac] = []
                mac_tot[mac].append(line)

                if ip not in ip_tot:
                    ip_tot[ip] = []
                ip_tot[ip].append(line)

            # affiche doublons mac
            for mac, lines in mac_tot.items():
                if len(lines) > 1:
                    print("duplicate mac addresses:")
                    for l in lines:
                        print(l)

            # affiche doublons ip
            for ip, lines in ip_tot.items():
                if len(lines) > 1:
                    print("duplicate ip addresses:")
                    for l in lines:
                        print(l)

        except NoValidConnectionsError:
            print('pas de connexion valide.')
        except SSHException as e:
            print(f'erreur ssh : {e}')

    else:
        # si aucun paramètre : vérifier tous les serveurs définis
        servers = config.get("dhcp-servers", {}).keys()
        for server_ip in servers:
            try:
                list_dhcp = dhcp_list(server_ip, config)
                mac_tot = {}
                ip_tot = {}

                for addr in list_dhcp:
                    mac = addr["mac"]
                    ip = addr["ip"]
                    line = f"dhcp-host={mac},{ip}"

                    if mac not in mac_tot:
                        mac_tot[mac] = []
                    mac_tot[mac].append(line)

                    if ip not in ip_tot:
                        ip_tot[ip] = []
                    ip_tot[ip].append(line)

                for mac, lines in mac_tot.items():
                    if len(lines) > 1:
                        print(f"{server_ip}: duplicate mac addresses:")
                        for l in lines:
                            print(l)

                for ip, lines in ip_tot.items():
                    if len(lines) > 1:
                        print(f"{server_ip}: duplicate ip addresses:")
                        for l in lines:
                            print(l)

            except NoValidConnectionsError:
                print(f'pas de connexion valide à {server_ip}')
            except SSHException as e:
                print(f'erreur ssh : {e}')


if __name__ == '__main__':
    main()

config_superviseur.yaml

---
dhcp_hosts_cfg: /etc/dnsmasq.d/hosts.conf

user: dhcp-mod

dhcp-servers:
    10.20.1.5: 10.20.1.0/24
    10.20.2.5: 10.20.2.0/24

config.py

import yaml
import os
import ipaddress

# charge une config YAML depuis un fichier
# si le fichier n'existe pas et create=True, crée un fichier minimal par défaut
def load_config(filename, create):
    minimal_config = {
        "dhcp_hosts_cfg": "/etc/dnsmasq.d/hosts.conf",
        "user": "dhcp-mod"
    }

    if os.path.exists(filename):
        # le fichier existe -> on le lit et on charge le YAML
        with open(filename, "r") as f:
            config = yaml.safe_load(f)
        return config
    else:
        if create == True:
            # le fichier n'existe pas -> on le crée avec une config de base
            with open(filename, "w") as f:
                yaml.dump(minimal_config, f)
            return minimal_config
        else:
            # ni fichier ni création -> on affiche une erreur et on renvoie None
            print("Error the file doesn't exist or the create parameter is FALSE")
            return None

# cherche le serveur DHCP correspondant à une ip donnée
# en parcourant la liste des réseaux déclarés
def get_dhcp_server(ip, cfg):
    ip_addr = ipaddress.IPv4Address(ip)  # convertit en objet IPv4
    for server_ip, network_str in cfg.get("dhcp-servers", {}).items():
        network = ipaddress.IPv4Network(network_str)
        if ip_addr in network:
            # l'ip appartient à ce réseau -> retourne ce serveur
            return {server_ip: network_str}

    return None # si aucune ip correspond -> return None

delete-dhcp-client.py

#!/usr/bin/env python3


import sys
from validation import if_macvalide  
from dhcp import dhcp_remove, mac_exists 
from config import load_config  
from fabric import Connection  
from paramiko.ssh_exception import NoValidConnectionsError, SSHException  


def main():
    # récupère l'argument MAC depuis la ligne de commande
    mac = sys.argv[1]

    # vérifie si l'adresse MAC est bien formatée
    if if_macvalide(mac):
        # charge la configuration du superviseur
        config = load_config('config_superviseur.yaml', True)

        # parcourt tous les serveurs DHCP définis
        for server_ip in config.get("dhcp-servers", {}).keys():
            # crée une connexion SSH au serveur en cours
            cnx = Connection(host=server_ip, user='dhcp-mod')
            try:
                # vérifie si la MAC existe sur ce serveur
                if mac_exists(cnx, mac, config):
                    # si oui, supprime l'entrée et quitte avec succès
                    dhcp_remove(mac, server_ip, config)
                    sys.exit(0)

            # gestion des erreurs de connexion SSH
            except NoValidConnectionsError:
                print('Pas de connexion valide.')
                sys.exit(1)
            except SSHException as e:
                print(f'Erreur de connexion SSH : {e}')
                sys.exit(1)

        # si aucun serveur n'a trouvé la MAC, affiche un message et quitte avec erreur
        print("MAC address not found")
        sys.exit(1)

    else:
        # si la MAC n'est pas valide, affiche une erreur et quitte avec erreur
        print("error: bad MAC address.")
        sys.exit(1)


if __name__ == '__main__':
    main()

dhcp.py

from fabric import Connection
from paramiko.ssh_exception import NoValidConnectionsError, SSHException


def ip_other_mac_exists(cnx, ip, mac, cfg):
    try:
        # récupère le chemin du fichier de config dhcp
        file_config = cfg['dhcp_hosts_cfg']
        # exécute une commande grep pour lister toutes les lignes dhcp-host=
        result = cnx.run(f"sudo grep '^dhcp-host=' {file_config}", hide=True, warn=True)

        # découpe le résultat en lignes
        lignes = result.stdout.strip().split('\n')
        # parcourt chaque ligne
        for ligne in lignes:
            if ligne.startswith("dhcp-host="):
                # prend la partie après =
                contenu = ligne.split("=", 1)[1]
                # découpe en mac et ip
                parts = contenu.split(",")
                if len(parts) >= 2:
                    mac_cfg, ip_cfg = parts[0], parts[1]
                    # vérifie si l'IP est la même mais pas la MAC
                    if ip == ip_cfg and mac != mac_cfg:
                        return True
        # aucun conflit détecté
        return False

    # gestion des erreurs de connexion SSH
    except NoValidConnectionsError:
        print('Pas de connexion valide.')
    except SSHException as e:
        print(f'Erreur de connexion SSH : {e}')



def mac_exists(cnx, mac, cfg):
    try:
        # chemin du fichier dhcp
        file_config = cfg['dhcp_hosts_cfg']
        # cherche la ligne dhcp-host pour cette MAC
        result = cnx.run(f"sudo grep 'dhcp-host={mac},' {file_config}", hide=True, warn=True)

        # retourne True si trouvé, False sinon
        if result.stdout.strip() != "":
            return True
        return False

    except NoValidConnectionsError:
        print('Pas de connexion valide.')
    except SSHException as e:
        print(f'Erreur de connexion SSH : {e}')



def dhcp_add(ip, mac, server, cfg):
    # crée une connexion SSH
    cx = Connection(host=server, user='dhcp-mod')

    try:
        # vérifie que l'IP n'est pas prise par une autre MAC
        if ip_other_mac_exists(cx, ip, mac, cfg):
            return "Error: IP address already use by another MAC"

        # vérifie si la MAC existe déjà
        grep_cmd = f"sudo grep 'dhcp-host={mac},' {cfg['dhcp_hosts_cfg']}"
        result = cx.run(grep_cmd, hide=True, warn=True)

        if result.stdout:
            # si la MAC existe : modifier la ligne avec sed
            sed_cmd = f"sudo sed -i s#dhcp-host={mac},.*#dhcp-host={mac},{ip}# {cfg['dhcp_hosts_cfg']}"
            cx.run(sed_cmd)
            # redémarrer le service dnsmasq pour appliquer la conf
            cx.run("sudo systemctl restart dnsmasq")
            return "MAC updated with new IP"

        else:
            # sinon : ajouter une nouvelle ligne avec tee
            add_cmd = f"echo 'dhcp-host={mac},{ip}' | sudo tee -a {cfg['dhcp_hosts_cfg']} > /dev/null"
            cx.run(add_cmd)
            # redémarrer dnsmasq
            cx.run("sudo systemctl restart dnsmasq")
            return f"dhcp-host={mac},{ip} added"

    except NoValidConnectionsError:
        return 'Error: No valid connection'
    except SSHException as e:
        return f'Error: SSH connection failed: {e}'



def dhcp_remove(mac, server, cfg):
    try:
        # crée la connexion SSH
        cx = Connection(host=server, user='dhcp-mod')
        file_config = cfg['dhcp_hosts_cfg']

        # vérifie si la MAC est présente
        grep_cmd = f"sudo grep 'dhcp-host={mac},' {file_config}"
        result = cx.run(grep_cmd, hide=True, warn=True)

        if result.stdout.strip() == "":
            return f"Error: MAC address {mac} not found in configuration"

        # supprime la ligne et nettoie les lignes vides éventuelles
        sed_cmd = f"sudo sed -i s#^dhcp-host={mac},.*## {file_config}"
        cx.run(sed_cmd)

        # redémarre dnsmasq
        cx.run("sudo systemctl restart dnsmasq")
        return f"Configuration for MAC {mac} removed"

    except NoValidConnectionsError:
        return 'Error: No valid SSH connection'
    except SSHException as e:
        return f'SSH Error: {e}'



def dhcp_list(server, cfg):
    try:
        # crée la connexion SSH
        cx = Connection(host=server, user='dhcp-mod')
        file_config = cfg['dhcp_hosts_cfg']

        # récupère toutes les lignes dhcp-host=
        result = cx.run(f"sudo grep '^dhcp-host=' {file_config}", hide=True, warn=True)
        lignes = result.stdout.strip().split('\n')

        output = []
        # pour chaque ligne, découpe en mac et ip et ajoute au résultat
        for ligne in lignes:
            if ligne.startswith("dhcp-host="):
                contenu = ligne.split("=", 1)[1]
                parts = contenu.split(",")
                if len(parts) >= 2:
                    mac, ip = parts[0], parts[1]
                    output.append({"mac": mac, "ip": ip})

        return output

    except NoValidConnectionsError:
        print('Error: No valid SSH connection')
        return []
    except SSHException as e:
        print(f'SSH Error: {e}')
        return []

list-dhcp.py

#!/usr/bin/env python3


import sys
from dhcp import dhcp_list  
from config import load_config  
from fabric import Connection  
from paramiko.ssh_exception import NoValidConnectionsError, SSHException  


def main():

    # variable pour savoir si on fait une recherche sur un serveur spécifique
    code_recherche = 0

    # si un argument est passé => on fait une recherche spécifique
    if len(sys.argv) > 1:
        code_recherche = 1
    # si trop de paramètres => erreur
    if len(sys.argv) > 2:
        print("error: Too much parameters")
        sys.exit(1)

    # charger la configuration supervisée depuis le fichier YAML
    config = load_config('config_superviseur.yaml', True)

    # si on a un paramètre : on liste sur un seul serveur
    if code_recherche == 1:
        server = sys.argv[1]  # adresse ou nom du serveur
        cnx = Connection(host=server, user='dhcp-mod')  # crée la connexion SSH (non utilisée ici car dhcp_list le fait déjà)

        try:
            # appel de dhcp_list pour récupérer les paires MAC/IP
            result = dhcp_list(server, config)
            # pour chaque entrée, affiche MAC et IP formatées avec alignement
            for lignes in result:
                print(f"{lignes['mac']:<20} {lignes['ip']}")
            sys.exit(0)

        # gestion des erreurs SSH
        except NoValidConnectionsError:
            print('Pas de connexion valide.')
        except SSHException as e:
            print(f'Erreur de connexion SSH : {e}')

    # si aucun paramètre : on liste pour tous les serveurs supervisés
    else:
        # liste des serveurs définis dans le YAML
        servers = list(config.get("dhcp-servers", {}).keys())

        try:
            # pour chaque serveur
            for server in servers:
                print(f"{server}:")  # affiche le nom du serveur
                result = dhcp_list(server, config)  # liste les paires MAC/IP pour ce serveur
                for lignes in result:
                    print(f"{lignes['mac']:<20} {lignes['ip']}")  # affiche chaque paire
            sys.exit(0)


        except NoValidConnectionsError:
            print('Pas de connexion valide.')
        except SSHException as e:
            print(f'Erreur de connexion SSH : {e}')



if __name__ == '__main__':
    main()

validation.py

import ipaddress

# vérifie si une adresse IPv4 est valide et pas spéciale
def if_ipv4valide(ip):
    try:
        ip = ipaddress.IPv4Address(ip)  # essaye de créer un objet IPv4
        # refuse si l'ip est vide, loopback, multicast, locale ou réservée
        if ip.is_unspecified or ip.is_loopback or ip.is_multicast or ip.is_link_local or ip.is_reserved:
            return False
        return True  # sinon c'est ok
    except ValueError:
        # erreur si le format n'est pas une ip valide
        return False

# liste des caractères valides pour une adresse MAC
carac = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
         'a', 'b', 'c', 'd', 'e', 'f',
         'A', 'B', 'C', 'D', 'E', 'F']

# vérifie si une adresse MAC est au bon format
def if_macvalide(mac):
    a = mac.split(':')  # coupe la mac en morceaux séparés par ':'
    if len(a) != 6:
        # doit avoir 6 parties
        return False
    for value in a:
        if len(value) != 2:
            # chaque partie doit avoir 2 caractères
            return False
        for charac in value:
            if charac not in carac:
                # chaque caractère doit être un chiffre ou une lettre hexadécimale
                return False
    return True