From 7ea0ee327b110b54d85e71b06af7342667232980 Mon Sep 17 00:00:00 2001 From: mmmohebi Date: Tue, 18 Nov 2025 12:24:33 +0330 Subject: [PATCH] fix(hoster.py): Refactor event handling and hosts file update Refactored the Docker event handling to use json parsing for more robust event processing. Updated the hosts file update logic to correctly handle existing sections and ensure atomic writes. Improved container info extraction and error handling. --- hoster.py | 212 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 129 insertions(+), 83 deletions(-) diff --git a/hoster.py b/hoster.py index ae6c47e..aeb4232 100644 --- a/hoster.py +++ b/hoster.py @@ -1,25 +1,27 @@ -#!/usr/bin/python3 import docker import argparse import shutil import signal -import time import sys -import os +import json label_name = "hoster.domains" -enclosing_pattern = "#-----------Docker-Hoster-Domains----------\n" +start_pattern = "#-----------Docker-Hoster-Domains----------\n" +end_pattern = "#-----Do-not-add-hosts-after-this-line-----\n" hosts_path = "/tmp/hosts" hosts = {} -def signal_handler(signal, frame): +dockerClient = docker.DockerClient(base_url="unix:///var/run/docker.sock") + + +def signal_handler(sig, frame): global hosts hosts = {} update_hosts_file() sys.exit(0) + def main(): - # register the exit signals signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) @@ -27,112 +29,156 @@ def main(): global hosts_path hosts_path = args.file - dockerClient = docker.APIClient(base_url='unix://%s' % args.socket) - events = dockerClient.events(decode=True) - #get running containers - for c in dockerClient.containers(quiet=True, all=False): - container_id = c["Id"] - container = get_container_data(dockerClient, container_id) - hosts[container_id] = container + global dockerClient + dockerClient = docker.DockerClient(base_url=f"unix://{args.socket}") + + # load running containers + for c in dockerClient.containers.list(): + hosts[c.id] = extract_container_info(c) update_hosts_file() - #listen for events to keep the hosts file updated - for e in events: - if e["Type"]!="container": - continue - - status = e["status"] - if status =="start": - container_id = e["id"] - container = get_container_data(dockerClient, container_id) - hosts[container_id] = container - update_hosts_file() + print("Listening for Docker events...") - if status=="stop" or status=="die" or status=="destroy": - container_id = e["id"] + # listen for container events + for raw in dockerClient.events(decode=False): + try: + event = json.loads(raw.decode("utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError): + continue + + if event.get("Type") != "container": + continue + + action = event.get("Action") + container_id = event.get("id") + + if action == "start": + c = get_container(container_id) + if c: + hosts[container_id] = extract_container_info(c) + update_hosts_file() + + elif action in ("stop", "die", "destroy"): if container_id in hosts: hosts.pop(container_id) update_hosts_file() - if status=="rename": - container_id = e["id"] - if container_id in hosts: - container = get_container_data(dockerClient, container_id) - hosts[container_id] = container + elif action == "rename": + c = get_container(container_id) + if c: + hosts[container_id] = extract_container_info(c) update_hosts_file() -def get_container_data(dockerClient, container_id): - #extract all the info with the docker api - info = dockerClient.inspect_container(container_id) - container_hostname = info["Config"]["Hostname"] - container_name = info["Name"].strip("/") - container_ip = info["NetworkSettings"]["IPAddress"] - if info["Config"]["Domainname"]: - container_hostname = container_hostname + "." + info["Config"]["Domainname"] - +def get_container(container_id): + try: + return dockerClient.containers.get(container_id) + except docker.errors.NotFound: + return None + + +def extract_container_info(container): + info = container.attrs + + name = info["Name"].strip("/") + hostname = info["Config"]["Hostname"] + domain = info["Config"]["Domainname"] + + if domain: + hostname = f"{hostname}.{domain}" + result = [] - for values in info["NetworkSettings"]["Networks"].values(): - - if not values["Aliases"]: + networks = info["NetworkSettings"]["Networks"] + for net_name, net in networks.items(): + ip = net.get("IPAddress") + if not ip: continue - result.append({ - "ip": values["IPAddress"] , - "name": container_name, - "domains": set(values["Aliases"] + [container_name, container_hostname]) - }) + aliases = net.get("Aliases") or [] + domains = set(aliases + [name, hostname]) - if container_ip: - result.append({"ip": container_ip, "name": container_name, "domains": [container_name, container_hostname ]}) + result.append({ + "ip": ip, + "name": name, + "domains": domains + }) + + # fallback IP + default_ip = info["NetworkSettings"].get("IPAddress") + if default_ip: + result.append({ + "ip": default_ip, + "name": name, + "domains": set([name, hostname]) + }) + + # add extra domains from label if present + labels = info["Config"]["Labels"] + extra_domains = [] + if label_name in labels: + extra = labels[label_name] + extra_domains = [d.strip() for d in extra.split(",") if d.strip()] + + for res in result: + res["domains"].update(extra_domains) return result def update_hosts_file(): - if len(hosts)==0: - print("Removing all hosts before exit...") + if len(hosts) == 0: + print("Clearing hosts file...") else: print("Updating hosts file with:") - for id,addresses in hosts.items(): + for id, addresses in hosts.items(): for addr in addresses: - print("ip: %s domains: %s" % (addr["ip"], addr["domains"])) + print(f"ip: {addr['ip']} domains: {addr['domains']}") - #read all the lines of thge original file - lines = [] - with open(hosts_path,"r+") as hosts_file: - lines = hosts_file.readlines() + # read existing hosts + try: + with open(hosts_path, "r") as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] - #remove all the lines after the known pattern - for i,line in enumerate(lines): - if line==enclosing_pattern: - lines = lines[:i] - break; + # remove existing section(s) properly + clean_lines = [] + i = 0 + while i < len(lines): + if lines[i] == start_pattern: + # skip until after end_pattern + while i < len(lines) and lines[i] != end_pattern: + i += 1 + if i < len(lines) and lines[i] == end_pattern: + i += 1 # skip the end_pattern + else: + clean_lines.append(lines[i]) + i += 1 - #remove all the trailing newlines on the line list - if lines: - while lines[-1].strip()=="": lines.pop() + lines = clean_lines - #append all the domain lines - if len(hosts)>0: - lines.append("\n\n"+enclosing_pattern) - - for id, addresses in hosts.items(): - for addr in addresses: - lines.append("%s %s\n"%(addr["ip"]," ".join(addr["domains"]))) - - lines.append("#-----Do-not-add-hosts-after-this-line-----\n\n") + # trim trailing empty lines + while lines and not lines[-1].strip(): + lines.pop() - #write it on the auxiliar file - aux_file_path = hosts_path+".aux" - with open(aux_file_path,"w") as aux_hosts: - aux_hosts.writelines(lines) + # add new entries + if hosts: + lines.append("\n") # single empty line before section + lines.append(start_pattern) + for id, addrs in hosts.items(): + for addr in addrs: + domains_str = " ".join(sorted(addr["domains"])) + lines.append(f"{addr['ip']}\t{domains_str}\n") + lines.append(end_pattern) - #replace etc/hosts with aux file, making it atomic - shutil.move(aux_file_path, hosts_path) + aux = hosts_path + ".aux" + with open(aux, "w") as f: + f.writelines(lines) + + shutil.move(aux, hosts_path) def parse_args(): @@ -141,6 +187,6 @@ def parse_args(): parser.add_argument('file', type=str, nargs="?", default="/tmp/hosts", help='The /etc/hosts file to sync the containers with.') return parser.parse_args() -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() \ No newline at end of file