Enhancements: Upgraded Actions, Improved Logging, Restructured Project (#1)
* update python project use setup tools, and upgrade github action version * add manual run trigger * add check not found check * remove TODO as not possible * Update doc and add default value of non required config --------- Co-authored-by: Snigdhajyoti Ghosh <snigdhasjg@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
f601634857
commit
2c9b686062
7
docker_nsupdate_ddns/default.config.env
Normal file
7
docker_nsupdate_ddns/default.config.env
Normal file
@@ -0,0 +1,7 @@
|
||||
DOCKER_SOCKET=/var/run/docker.sock
|
||||
HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname
|
||||
IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore
|
||||
DNS_RECORD_TTL=60
|
||||
REFRESH_INTERVAL=60
|
||||
ONE_SHOT=False
|
||||
DEFAULT_NETWORK=None
|
||||
1
docker_nsupdate_ddns/lib/__init__.py
Normal file
1
docker_nsupdate_ddns/lib/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["container", "nsupdate"]
|
||||
66
docker_nsupdate_ddns/lib/container.py
Normal file
66
docker_nsupdate_ddns/lib/container.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import logging
|
||||
|
||||
import docker
|
||||
|
||||
config = {}
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_container_name(container):
|
||||
"""
|
||||
Get name of container, try in the following order:
|
||||
- Check if hostname_label is set
|
||||
- Fall back to container Name
|
||||
"""
|
||||
x = container.attrs['Name'][1:]
|
||||
|
||||
if config['HOSTNAME_LABEL'] in container.attrs['Config']['Labels']:
|
||||
x = container.attrs['Config']['Labels'][config['HOSTNAME_LABEL']]
|
||||
|
||||
x = x.replace("_", "-") # Be compliant with RFC1035
|
||||
return x
|
||||
|
||||
|
||||
def get_container_ip(container):
|
||||
"""
|
||||
Get IP of container. Try in the following order
|
||||
- default_network
|
||||
- First found network
|
||||
- Fall back to ['NetworkSettings']['IPAddress']
|
||||
"""
|
||||
x = container.attrs['NetworkSettings']['IPAddress']
|
||||
|
||||
if next(iter(container.attrs['NetworkSettings']['Networks'])):
|
||||
network_name = next(
|
||||
iter(container.attrs['NetworkSettings']['Networks']))
|
||||
x = container.attrs['NetworkSettings']['Networks'][network_name]['IPAddress']
|
||||
|
||||
if config['DEFAULT_NETWORK'] in container.attrs['NetworkSettings']['Networks']:
|
||||
x = container.attrs['NetworkSettings']['Networks'][config['DEFAULT_NETWORK']]['IPAddress']
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def generate_container_list():
|
||||
client = docker.from_env()
|
||||
|
||||
container_list = client.containers.list()
|
||||
ipam4 = {}
|
||||
|
||||
for container in container_list:
|
||||
if config['IGNORE_LABEL'] in container.attrs['Config']['Labels']:
|
||||
LOG.debug(f"Ignoring container {container.attrs['Name']} as ignore label present")
|
||||
continue
|
||||
|
||||
container_name = get_container_name(container)
|
||||
container_ip = get_container_ip(container)
|
||||
if container_ip:
|
||||
ipam4[container_name] = container_ip
|
||||
|
||||
return ipam4
|
||||
|
||||
|
||||
def init(_config):
|
||||
global config
|
||||
config = _config
|
||||
44
docker_nsupdate_ddns/lib/nsupdate.py
Normal file
44
docker_nsupdate_ddns/lib/nsupdate.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import logging
|
||||
|
||||
import dns.update
|
||||
import dns.query
|
||||
import dns.tsigkeyring
|
||||
import ipaddress
|
||||
|
||||
config = {}
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_records(records):
|
||||
keyring = dns.tsigkeyring.from_text({config['TSIG_NAME']: config['TSIG_KEY']})
|
||||
|
||||
for hostname, ip in records.items():
|
||||
LOG.info(f"Adding record for {hostname}({ip})")
|
||||
|
||||
rrtype = "A"
|
||||
address = ipaddress.ip_address(ip)
|
||||
if isinstance(address, ipaddress.IPv4Address):
|
||||
rrtype = "A"
|
||||
if isinstance(address, ipaddress.IPv6Address):
|
||||
rrtype = "AAAA"
|
||||
|
||||
update = dns.update.Update(config['DOMAIN'], keyring=keyring)
|
||||
update.add(hostname, int(config['DNS_RECORD_TTL']), rrtype, ip)
|
||||
dns.query.tcp(update, config['NAMESERVER'], timeout=2)
|
||||
|
||||
|
||||
def delete_records(records):
|
||||
keyring = dns.tsigkeyring.from_text({config['TSIG_NAME']: config['TSIG_KEY']})
|
||||
|
||||
for hostname, ip in records.items():
|
||||
LOG.info(f"Deleting record for {hostname}({ip})")
|
||||
|
||||
update = dns.update.Update(config['DOMAIN'], keyring=keyring)
|
||||
update.delete(hostname)
|
||||
dns.query.tcp(update, config['NAMESERVER'], timeout=2)
|
||||
|
||||
|
||||
def init(_config):
|
||||
global config
|
||||
config = _config
|
||||
105
docker_nsupdate_ddns/main.py
Executable file
105
docker_nsupdate_ddns/main.py
Executable file
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from dotenv import dotenv_values
|
||||
import time
|
||||
|
||||
from docker_nsupdate_ddns.lib import *
|
||||
import logging
|
||||
|
||||
config = {}
|
||||
ipam4_old = {}
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
global config
|
||||
config = get_config()
|
||||
check_required_vars(config)
|
||||
|
||||
if not eval(config['ONE_SHOT']):
|
||||
while True:
|
||||
loop()
|
||||
time.sleep(int(config['REFRESH_INTERVAL']))
|
||||
|
||||
loop()
|
||||
LOG.info("Ending the process as ONE_SHOT is True")
|
||||
|
||||
|
||||
def check_required_vars(_config):
|
||||
# Check for all required config
|
||||
required_vars = [
|
||||
'DOMAIN',
|
||||
'NAMESERVER',
|
||||
'TSIG_NAME',
|
||||
'DOCKER_SOCKET',
|
||||
'HOSTNAME_LABEL',
|
||||
'IGNORE_LABEL',
|
||||
'DNS_RECORD_TTL',
|
||||
'DEFAULT_NETWORK',
|
||||
'REFRESH_INTERVAL',
|
||||
'ONE_SHOT'
|
||||
]
|
||||
missing_vars = []
|
||||
for item in required_vars:
|
||||
if item in _config:
|
||||
LOG.info(f"Detected config value: {item}={_config[item]}")
|
||||
else:
|
||||
missing_vars.append(item)
|
||||
if 'TSIG_KEY' not in _config:
|
||||
# Don't log it as it's a secret
|
||||
missing_vars.append('TSIG_KEY')
|
||||
if len(missing_vars) > 1:
|
||||
LOG.error(f"Missing required config: {', '.join(missing_vars)}")
|
||||
exit(1)
|
||||
|
||||
# Check if docker socket is correct
|
||||
try:
|
||||
if not stat.S_ISSOCK(os.stat(_config['DOCKER_SOCKET']).st_mode):
|
||||
LOG.error(f"{_config['DOCKER_SOCKET']} not a docker socket file, exiting...")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
LOG.error(f"Docker socket {_config['DOCKER_SOCKET']} not found.", e)
|
||||
raise e
|
||||
|
||||
|
||||
def loop():
|
||||
container.init(config)
|
||||
ipam4 = container.generate_container_list()
|
||||
global ipam4_old
|
||||
|
||||
additions4 = determine_additions(ipam4, ipam4_old)
|
||||
deletions4 = determine_deletions(ipam4, ipam4_old)
|
||||
|
||||
nsupdate.init(config)
|
||||
nsupdate.delete_records(deletions4)
|
||||
nsupdate.add_records(additions4)
|
||||
|
||||
ipam4_old = ipam4
|
||||
|
||||
|
||||
def get_config():
|
||||
config_file = sys.argv[1] if len(sys.argv) >= 2 else 'config.env'
|
||||
|
||||
x = {
|
||||
**dotenv_values(os.path.join(os.path.dirname(__file__), 'default.config.env')),
|
||||
**dotenv_values(os.path.join(os.getcwd(), config_file)),
|
||||
**os.environ
|
||||
}
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def determine_additions(ipam, ipam_old):
|
||||
return {k: v for k, v in ipam.items() if k not in ipam_old}
|
||||
|
||||
|
||||
def determine_deletions(ipam, ipam_old):
|
||||
return {k: v for k, v in ipam_old.items() if k not in ipam}
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user