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