Initial version
This commit is contained in:
25
.editorconfig
Normal file
25
.editorconfig
Normal file
@@ -0,0 +1,25 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
|
||||
# 4 space indentation
|
||||
[*.{py,java,r,R}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# 2 space indentation
|
||||
[*.{js,json,y{a,}ml,html,cwl}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{md,Rmd,rst}]
|
||||
trim_trailing_whitespace = false
|
||||
indent_style = space
|
||||
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
*.swp
|
||||
config
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
docker-nsupdate-ddns
|
||||
8
.idea/docker-nsupdate-ddns.iml
generated
Normal file
8
.idea/docker-nsupdate-ddns.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Pipenv (docker-nsupdate-ddns)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Pipenv (docker-bind-ddns)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/../docker-nsupdate-ddns/.idea/docker-nsupdate-ddns.iml" filepath="$PROJECT_DIR$/../docker-nsupdate-ddns/.idea/docker-nsupdate-ddns.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
14
Pipfile
Normal file
14
Pipfile
Normal file
@@ -0,0 +1,14 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
docker = "*"
|
||||
python-dotenv = "*"
|
||||
dnspython = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
89
Pipfile.lock
generated
Normal file
89
Pipfile.lock
generated
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "2a9a9c218218349a7c550a97bb3bc3b6113227211c48e027755bdc8baf5822a1"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||
],
|
||||
"version": "==2022.6.15"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
|
||||
"sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
"sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0",
|
||||
"sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f",
|
||||
"sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.20.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
|
||||
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
|
||||
],
|
||||
"version": "==2.27.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec",
|
||||
"sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"
|
||||
],
|
||||
"version": "==1.26.10"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:074e2ed575e7c822fc0940d31c3ac9bb2b1142c303eafcf3e304e6ce035522e8",
|
||||
"sha256:6278a75065395418283f887de7c3beafb3aa68dada5cacbe4b214e8d26da499b"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
100
README.md
Normal file
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# docker-nsupdate-ddns
|
||||
|
||||
This script pushes container/ip information from a Docker instance to a DNS server via the DNS update mechanism desribed in RFC 2136 (nsupdate). nsupdate is implemented by ISC Bind9.
|
||||
|
||||
Every REFRESH_INTERVAL seconds, it queries all the Docker containers on the local host, finds their IP and pushes it with the name to the DNS server.
|
||||
|
||||
## Running
|
||||
|
||||
The script takes environment variables or a config file. A sample config file is provided in `config.sample`. The file name of the config file can be passed as argument, but defaults to `config`.
|
||||
|
||||
The names of the environment variables is the same as in the config file. Environment variables have precendence over the config file.
|
||||
|
||||
### Environment variables
|
||||
```bash
|
||||
docker run -d \
|
||||
-e DOMAIN=int.mtak.nl \
|
||||
-e HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname \
|
||||
-e DEFAULT_NETWORK=10.100.0.192/26 \
|
||||
-e REFRESH_INTERVAL=5 \
|
||||
-e ONE_SHOT=True \
|
||||
-e NAMESERVER=10.100.0.11 \
|
||||
-e TSIG_NAME=dck1 \
|
||||
-e TSIG_KEY=SyYXDCJ4kIs3qhvI= \
|
||||
merijntjetak/docker-bind/ddns:latest
|
||||
```
|
||||
|
||||
### Config file
|
||||
```bash
|
||||
cat <<EOF >configfile
|
||||
DOMAIN=int.mtak.nl
|
||||
HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname
|
||||
DEFAULT_NETWORK=10.100.0.192/26
|
||||
REFRESH_INTERVAL=5
|
||||
ONE_SHOT=True
|
||||
NAMESERVER=10.100.0.11
|
||||
TSIG_NAME=dck1
|
||||
TSIG_KEY=SyYXDCJ4kIs3qhvI=
|
||||
EOF
|
||||
|
||||
docker run -d \
|
||||
-v `pwd`/configfile:/configfile
|
||||
merijntjetak/docker-bind-ddns:latest /configfile
|
||||
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
- `DOMAIN` - Sets the domain in which the records are created. Needs to match the Bind zone.
|
||||
- `HOSTNAME_LABEL` - Docker label to override the default record name with. Use with `docker --label=nl.mtak.docker-bind-ddns.hostname=nginx` to get `nginx.int.mtak.nl`
|
||||
- `DEFAULT_NETWORK` - Preferred network to find IP for, in case there are multiple networks
|
||||
- `REFRESH_INTERVAL` - Interval between updates
|
||||
- `ONE_SHOT` - Set to True for the script to update once and immediately quit (nice for debugging)
|
||||
- `NAMESERVER` - Nameserver to push updates to
|
||||
- `TSIG_NAME` - Name of the TSIG key
|
||||
- `TSIG_KEY` - TSIG-KEY
|
||||
|
||||
### Bind9 integration
|
||||
|
||||
1. Generate a key
|
||||
|
||||
`tsig-keygen clientname >/etc/bind/keys/clientname.key`
|
||||
|
||||
2. Include keys in your Bind9 configuration
|
||||
|
||||
`include "/etc/bind/keys/*";`
|
||||
|
||||
3. Allow updates to your zone:
|
||||
|
||||
```
|
||||
zone "int.mtak.nl" {
|
||||
type master;
|
||||
file "/etc/bind/db/int.mtak.nl.zone";
|
||||
update-policy {
|
||||
grant clientname zonesub ANY;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Design
|
||||
### Requirements
|
||||
|
||||
- Eventual redunancy (Bind9 zone transfers to secondary)
|
||||
- Support for multiple individual Docker servers
|
||||
- IPv6 support
|
||||
- Detect hostname in decreasing order of priority:
|
||||
- label
|
||||
- Container name
|
||||
- Forward-only
|
||||
- Clean up DNS (DNS is stateful but the script isn't, so there might be a mismatch)
|
||||
|
||||
### Todo
|
||||
|
||||
- Make event-driven with [Ahab](https://github.com/instacart/ahab)
|
||||
- Add tests
|
||||
|
||||
### Alternatives
|
||||
|
||||
CoreDNS didn't fit the requirements, because zone transfers out of a CoreDNS server do not
|
||||
include the records from the coredns-dockerdiscovery plugin.
|
||||
|
||||
8
config.sample
Normal file
8
config.sample
Normal file
@@ -0,0 +1,8 @@
|
||||
DOMAIN=int.mtak.nl
|
||||
HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname
|
||||
DEFAULT_NETWORK=10.100.0.192/26
|
||||
REFRESH_INTERVAL=10
|
||||
ONE_SHOT=True
|
||||
NAMESERVER=10.100.0.11
|
||||
TSIG_NAME=dck1
|
||||
TSIG_KEY=SyYhvI=
|
||||
1
lib/__init__.py
Normal file
1
lib/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["container", "nsupdate"]
|
||||
57
lib/container.py
Normal file
57
lib/container.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import docker
|
||||
|
||||
config = {}
|
||||
|
||||
|
||||
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']]
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def generate_container_list():
|
||||
client = docker.from_env()
|
||||
|
||||
container_list = client.containers.list()
|
||||
ipam4 = {}
|
||||
|
||||
for container in container_list:
|
||||
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
|
||||
37
lib/nsupdate.py
Normal file
37
lib/nsupdate.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import dns.update
|
||||
import dns.query
|
||||
import dns.tsigkeyring
|
||||
import ipaddress
|
||||
|
||||
config = {}
|
||||
|
||||
|
||||
def add_records(records):
|
||||
keyring = dns.tsigkeyring.from_text({config['tsig_name']: config['tsig_key']})
|
||||
for hostname, ip in records.items():
|
||||
print("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, 60, 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():
|
||||
print("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
|
||||
64
main.py
Executable file
64
main.py
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
import time
|
||||
|
||||
from lib import *
|
||||
|
||||
config = {}
|
||||
ipam4_old = {}
|
||||
|
||||
|
||||
def main():
|
||||
get_config()
|
||||
|
||||
if not config['one_shot']:
|
||||
while True:
|
||||
loop()
|
||||
time.sleep(config['refresh_interval'])
|
||||
|
||||
loop()
|
||||
|
||||
|
||||
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'
|
||||
load_dotenv(config_file)
|
||||
|
||||
config['domain'] = os.environ.get("DOMAIN")
|
||||
config['hostname_label'] = os.environ.get("HOSTNAME_LABEL")
|
||||
config['default_network'] = os.environ.get("DEFAULT_NETWORK")
|
||||
config['refresh_interval'] = int(os.environ.get("REFRESH_INTERVAL"))
|
||||
config['one_shot'] = os.environ.get("ONE_SHOT").lower() in ['true', 'yes']
|
||||
config['nameserver'] = os.environ.get("NAMESERVER")
|
||||
config['tsig_name'] = os.environ.get("TSIG_NAME")
|
||||
config['tsig_key'] = os.environ.get("TSIG_KEY")
|
||||
|
||||
|
||||
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