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.
This commit is contained in:
parent
aaf88f526c
commit
7ea0ee327b
204
hoster.py
204
hoster.py
|
|
@ -1,25 +1,27 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
import docker
|
import docker
|
||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import json
|
||||||
|
|
||||||
label_name = "hoster.domains"
|
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_path = "/tmp/hosts"
|
||||||
hosts = {}
|
hosts = {}
|
||||||
|
|
||||||
def signal_handler(signal, frame):
|
dockerClient = docker.DockerClient(base_url="unix:///var/run/docker.sock")
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
global hosts
|
global hosts
|
||||||
hosts = {}
|
hosts = {}
|
||||||
update_hosts_file()
|
update_hosts_file()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# register the exit signals
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
|
@ -27,112 +29,156 @@ def main():
|
||||||
global hosts_path
|
global hosts_path
|
||||||
hosts_path = args.file
|
hosts_path = args.file
|
||||||
|
|
||||||
dockerClient = docker.APIClient(base_url='unix://%s' % args.socket)
|
global dockerClient
|
||||||
events = dockerClient.events(decode=True)
|
dockerClient = docker.DockerClient(base_url=f"unix://{args.socket}")
|
||||||
#get running containers
|
|
||||||
for c in dockerClient.containers(quiet=True, all=False):
|
# load running containers
|
||||||
container_id = c["Id"]
|
for c in dockerClient.containers.list():
|
||||||
container = get_container_data(dockerClient, container_id)
|
hosts[c.id] = extract_container_info(c)
|
||||||
hosts[container_id] = container
|
|
||||||
|
|
||||||
update_hosts_file()
|
update_hosts_file()
|
||||||
|
|
||||||
#listen for events to keep the hosts file updated
|
print("Listening for Docker events...")
|
||||||
for e in events:
|
|
||||||
if e["Type"]!="container":
|
# listen for container events
|
||||||
|
for raw in dockerClient.events(decode=False):
|
||||||
|
try:
|
||||||
|
event = json.loads(raw.decode("utf-8"))
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
status = e["status"]
|
if event.get("Type") != "container":
|
||||||
if status =="start":
|
continue
|
||||||
container_id = e["id"]
|
|
||||||
container = get_container_data(dockerClient, container_id)
|
|
||||||
hosts[container_id] = container
|
|
||||||
update_hosts_file()
|
|
||||||
|
|
||||||
if status=="stop" or status=="die" or status=="destroy":
|
action = event.get("Action")
|
||||||
container_id = e["id"]
|
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:
|
if container_id in hosts:
|
||||||
hosts.pop(container_id)
|
hosts.pop(container_id)
|
||||||
update_hosts_file()
|
update_hosts_file()
|
||||||
|
|
||||||
if status=="rename":
|
elif action == "rename":
|
||||||
container_id = e["id"]
|
c = get_container(container_id)
|
||||||
if container_id in hosts:
|
if c:
|
||||||
container = get_container_data(dockerClient, container_id)
|
hosts[container_id] = extract_container_info(c)
|
||||||
hosts[container_id] = container
|
|
||||||
update_hosts_file()
|
update_hosts_file()
|
||||||
|
|
||||||
|
|
||||||
def get_container_data(dockerClient, container_id):
|
def get_container(container_id):
|
||||||
#extract all the info with the docker api
|
try:
|
||||||
info = dockerClient.inspect_container(container_id)
|
return dockerClient.containers.get(container_id)
|
||||||
container_hostname = info["Config"]["Hostname"]
|
except docker.errors.NotFound:
|
||||||
container_name = info["Name"].strip("/")
|
return None
|
||||||
container_ip = info["NetworkSettings"]["IPAddress"]
|
|
||||||
if info["Config"]["Domainname"]:
|
|
||||||
container_hostname = container_hostname + "." + info["Config"]["Domainname"]
|
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 = []
|
result = []
|
||||||
|
|
||||||
for values in info["NetworkSettings"]["Networks"].values():
|
networks = info["NetworkSettings"]["Networks"]
|
||||||
|
for net_name, net in networks.items():
|
||||||
if not values["Aliases"]:
|
ip = net.get("IPAddress")
|
||||||
|
if not ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result.append({
|
aliases = net.get("Aliases") or []
|
||||||
"ip": values["IPAddress"] ,
|
domains = set(aliases + [name, hostname])
|
||||||
"name": container_name,
|
|
||||||
"domains": set(values["Aliases"] + [container_name, container_hostname])
|
|
||||||
})
|
|
||||||
|
|
||||||
if container_ip:
|
result.append({
|
||||||
result.append({"ip": container_ip, "name": container_name, "domains": [container_name, container_hostname ]})
|
"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
|
return result
|
||||||
|
|
||||||
|
|
||||||
def update_hosts_file():
|
def update_hosts_file():
|
||||||
if len(hosts)==0:
|
if len(hosts) == 0:
|
||||||
print("Removing all hosts before exit...")
|
print("Clearing hosts file...")
|
||||||
else:
|
else:
|
||||||
print("Updating hosts file with:")
|
print("Updating hosts file with:")
|
||||||
|
|
||||||
for id,addresses in hosts.items():
|
for id, addresses in hosts.items():
|
||||||
for addr in addresses:
|
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
|
# read existing hosts
|
||||||
lines = []
|
try:
|
||||||
with open(hosts_path,"r+") as hosts_file:
|
with open(hosts_path, "r") as f:
|
||||||
lines = hosts_file.readlines()
|
lines = f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
lines = []
|
||||||
|
|
||||||
#remove all the lines after the known pattern
|
# remove existing section(s) properly
|
||||||
for i,line in enumerate(lines):
|
clean_lines = []
|
||||||
if line==enclosing_pattern:
|
i = 0
|
||||||
lines = lines[:i]
|
while i < len(lines):
|
||||||
break;
|
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
|
lines = clean_lines
|
||||||
if lines:
|
|
||||||
while lines[-1].strip()=="": lines.pop()
|
|
||||||
|
|
||||||
#append all the domain lines
|
# trim trailing empty lines
|
||||||
if len(hosts)>0:
|
while lines and not lines[-1].strip():
|
||||||
lines.append("\n\n"+enclosing_pattern)
|
lines.pop()
|
||||||
|
|
||||||
for id, addresses in hosts.items():
|
# add new entries
|
||||||
for addr in addresses:
|
if hosts:
|
||||||
lines.append("%s %s\n"%(addr["ip"]," ".join(addr["domains"])))
|
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)
|
||||||
|
|
||||||
lines.append("#-----Do-not-add-hosts-after-this-line-----\n\n")
|
aux = hosts_path + ".aux"
|
||||||
|
with open(aux, "w") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
#write it on the auxiliar file
|
shutil.move(aux, hosts_path)
|
||||||
aux_file_path = hosts_path+".aux"
|
|
||||||
with open(aux_file_path,"w") as aux_hosts:
|
|
||||||
aux_hosts.writelines(lines)
|
|
||||||
|
|
||||||
#replace etc/hosts with aux file, making it atomic
|
|
||||||
shutil.move(aux_file_path, hosts_path)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
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.')
|
parser.add_argument('file', type=str, nargs="?", default="/tmp/hosts", help='The /etc/hosts file to sync the containers with.')
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue