Initial Import

This commit is contained in:
2019-10-14 17:00:55 +13:00
commit ef1d2ca6c9
5 changed files with 348 additions and 0 deletions

133
.gitignore vendored Normal file
View 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
View 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
View File

146
lib/directadmin.py Normal file
View 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
View 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