You've already forked da-ddnsclient
Initial Import
This commit is contained in:
133
.gitignore
vendored
Normal file
133
.gitignore
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Example user template template
|
||||||
|
### Example user template
|
||||||
|
|
||||||
|
# IntelliJ project files
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
out
|
||||||
|
gen
|
||||||
|
### Python template
|
||||||
|
# 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/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
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
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.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
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
62
ddns_updater.py
Normal file
62
ddns_updater.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
import tldextract
|
||||||
|
|
||||||
|
from lib.directadmin import DirectAdminClient, DirectAdminClientException
|
||||||
|
|
||||||
|
|
||||||
|
def get_zone_and_name(record_domain):
|
||||||
|
"""Find a suitable zone for a domain
|
||||||
|
:param str record_name: the domain name
|
||||||
|
:returns: (the zone, the name in the zone)
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
(subdomain, domain, suffix) = tldextract.extract(record_domain)
|
||||||
|
|
||||||
|
directadmin_zone = ".".join((domain, suffix))
|
||||||
|
directadmin_name = subdomain
|
||||||
|
|
||||||
|
return directadmin_zone, directadmin_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_directadmin_client(url, username, password):
|
||||||
|
return DirectAdminClient(
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option('--hostname', required=False, help='The FQDN you wish to update for DynamicDNS on DirectAdmin server')
|
||||||
|
@click.option('--url', required=False, help='The FQDN to access DirectAdmin server e.g https://my.directadmin.com:2222')
|
||||||
|
@click.option('--username', required=False, help='The username for your account on DirectAdmin server')
|
||||||
|
@click.option('--password', required=False, help='The password for your account on DirectAdmin server')
|
||||||
|
def main(hostname, url, username, password):
|
||||||
|
click.echo('Updating DNS for ' + hostname)
|
||||||
|
(directadmin_zone, directadmin_name) = get_zone_and_name(hostname)
|
||||||
|
|
||||||
|
da_client = get_directadmin_client(url, username, password)
|
||||||
|
current_ip = requests.get('https://icanhazip.com').text.strip()
|
||||||
|
hostname_resolves_to = socket.gethostbyname(hostname).strip()
|
||||||
|
|
||||||
|
click.echo(current_ip)
|
||||||
|
click.echo(hostname_resolves_to)
|
||||||
|
|
||||||
|
if current_ip != hostname_resolves_to:
|
||||||
|
try:
|
||||||
|
response = da_client.update_dns_record(directadmin_zone,
|
||||||
|
"A",
|
||||||
|
directadmin_name,
|
||||||
|
hostname_resolves_to,
|
||||||
|
current_ip, 30)
|
||||||
|
if int(response['error']) == 0:
|
||||||
|
click.echo("Successfully updated A record for " + hostname)
|
||||||
|
except DirectAdminClientException as e:
|
||||||
|
click.echo("Error updating A record: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
0
lib/__init__.py
Normal file
0
lib/__init__.py
Normal file
146
lib/directadmin.py
Normal file
146
lib/directadmin.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import base64
|
||||||
|
from collections import OrderedDict
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
try:
|
||||||
|
# python 3
|
||||||
|
from urllib.request import urlopen, Request
|
||||||
|
from urllib.parse import urlencode, parse_qs
|
||||||
|
except ImportError:
|
||||||
|
# python 2
|
||||||
|
from urllib import urlencode
|
||||||
|
from urllib2 import urlopen, Request
|
||||||
|
from cgi import parse_qs
|
||||||
|
|
||||||
|
|
||||||
|
class DirectAdminClient:
|
||||||
|
|
||||||
|
def __init__(self, url, username, password):
|
||||||
|
|
||||||
|
self.version = "0.0.5"
|
||||||
|
self.client = requests.session()
|
||||||
|
self.headers = {'user-agent': 'pyDirectAdmin/' + str(self.version),
|
||||||
|
'Authorization': 'Basic %s' % base64.b64encode(("%s:%s" %
|
||||||
|
(username, password))
|
||||||
|
.encode()).decode('utf8'),
|
||||||
|
}
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def make_request(self, endpoint, data=None):
|
||||||
|
response = None # Empty response variable
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
# Data is present add the fields to call
|
||||||
|
response = urlopen(
|
||||||
|
Request(
|
||||||
|
"%s?%s" % (self.url + '/{}'.format(endpoint), urlencode(data)),
|
||||||
|
headers=self.headers,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif data is None:
|
||||||
|
# Data is not present so we don't need addition field.
|
||||||
|
response = urlopen(
|
||||||
|
Request(
|
||||||
|
"%s?" % (self.url + '/{}'.format(endpoint)),
|
||||||
|
headers=self.headers,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __process_response__(response):
|
||||||
|
if int(response['error'][0]) > 0:
|
||||||
|
# There was an error
|
||||||
|
raise DirectAdminClientException(response['text'][0])
|
||||||
|
elif int(response['error'][0]) == 0:
|
||||||
|
# Everything succeeded
|
||||||
|
return {'error': response['error'][0],
|
||||||
|
'message': response['text'][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_domain_list(self):
|
||||||
|
r = self.make_request('CMD_API_SHOW_DOMAINS')
|
||||||
|
|
||||||
|
domains = parse_qs(r.read().decode('utf8'),
|
||||||
|
keep_blank_values=0,
|
||||||
|
strict_parsing=1)
|
||||||
|
response = list()
|
||||||
|
for domain in domains.values():
|
||||||
|
response.append(domain[0])
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_zone_list(self, domain):
|
||||||
|
params = OrderedDict([('domain', domain)])
|
||||||
|
r = self.make_request('CMD_API_DNS_CONTROL', params)
|
||||||
|
|
||||||
|
return r.read()
|
||||||
|
|
||||||
|
def add_dns_record(self, domain, record_type, record_name, record_value, record_ttl=None):
|
||||||
|
|
||||||
|
params = OrderedDict([('domain', domain),
|
||||||
|
('action', 'add'),
|
||||||
|
('type', record_type.upper()),
|
||||||
|
('name', record_name),
|
||||||
|
('value', record_value)])
|
||||||
|
|
||||||
|
if record_ttl is not None:
|
||||||
|
params.update({'ttl': record_ttl})
|
||||||
|
|
||||||
|
response = self.make_request('CMD_API_DNS_CONTROL', data=params)
|
||||||
|
response = parse_qs(response.read().decode('utf8'),
|
||||||
|
keep_blank_values=0,
|
||||||
|
strict_parsing=1)
|
||||||
|
|
||||||
|
return self.__process_response__(response)
|
||||||
|
|
||||||
|
def update_dns_record(self, domain, record_type, record_name, record_value_old, record_value_new, record_ttl=None):
|
||||||
|
|
||||||
|
params = OrderedDict([('domain', domain),
|
||||||
|
('action', 'edit'),
|
||||||
|
('type', record_type.upper()),
|
||||||
|
(record_type.lower() + "recs0", 'name={}&value={}'.format(record_name, record_value_old)),
|
||||||
|
('name', record_name),
|
||||||
|
('value', record_value_new)])
|
||||||
|
|
||||||
|
if record_ttl is not None:
|
||||||
|
params.update({'ttl': record_ttl})
|
||||||
|
|
||||||
|
response = self.make_request('CMD_API_DNS_CONTROL', data=params)
|
||||||
|
response = parse_qs(response.read().decode('utf8'),
|
||||||
|
keep_blank_values=0,
|
||||||
|
strict_parsing=1)
|
||||||
|
|
||||||
|
return self.__process_response__(response)
|
||||||
|
|
||||||
|
def delete_dns_record(self, domain, record_type, record_name, record_value):
|
||||||
|
params = OrderedDict([('domain', domain),
|
||||||
|
('action', 'select'),
|
||||||
|
(record_type.lower() + "recs0", 'name={}&value={}'.format(record_name, record_value))
|
||||||
|
])
|
||||||
|
|
||||||
|
response = self.make_request('CMD_API_DNS_CONTROL', data=params)
|
||||||
|
response = parse_qs(response.read().decode('utf8'),
|
||||||
|
keep_blank_values=0,
|
||||||
|
strict_parsing=1)
|
||||||
|
|
||||||
|
return self.__process_response__(response)
|
||||||
|
|
||||||
|
def override_domain_ttl(self, domain, record_name, ttl):
|
||||||
|
params = OrderedDict([('domain', domain),
|
||||||
|
('action', 'ttl'),
|
||||||
|
('ttl_select', 'custom'),
|
||||||
|
('name', record_name),
|
||||||
|
('ttl', ttl)])
|
||||||
|
|
||||||
|
response = self.make_request('CMD_API_DNS_CONTROL', data=params)
|
||||||
|
response = parse_qs(response.read().decode('utf8'),
|
||||||
|
keep_blank_values=0,
|
||||||
|
strict_parsing=1)
|
||||||
|
|
||||||
|
return self.__process_response__(response)
|
||||||
|
|
||||||
|
|
||||||
|
class DirectAdminClientException(Exception):
|
||||||
|
pass
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
certifi==2019.9.11
|
||||||
|
chardet==3.0.4
|
||||||
|
Click==7.0
|
||||||
|
idna==2.8
|
||||||
|
requests==2.22.0
|
||||||
|
urllib3==1.25.6
|
||||||
|
tldextract==2.2.1
|
||||||
Reference in New Issue
Block a user