initial commit

This commit is contained in:
Kristian Krsnik 2023-07-26 00:49:47 +02:00
commit 7e8356a580
3 changed files with 155 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
config.json
log.txt

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# Dyn-Gandi
A DNS record updater for [Gandi's LiveDNS](https://api.gandi.net/docs/livedns/) API.
This script is heavily inspired by [dyn-gandi](https://github.com/Danamir/dyn-gandi).
## How it works
This script determines the your server's current IP by querying the resolvers defined in the config file.
After that it reads the current state of the domains and subdomains you specified of Gandi.
Should the IP address of a subdomain not match your current IP it will be updated.
The subdomain will be created should it not already exist.
## Note
The current implementation only allows for one entry per subdomain.
Meaning that should you have a TXT and a CNAME record for a subdomain that is in the config file, then both these entries will be deleted and replaced by a single A name record.
## How to use
First, get your API key from https://account.gandi.net/en/users/USER/security where `USER` is your Gandi username.
On first run the program will create a minimal, yet complete `config.json` in the same directory it is being run in.
Next, enter the API key into the configuration file and assign domains to it.
The domains are keys to a list of subdomain for a given domain you wish to monitor.
The below example is complete and should explain itself.
Resolvers are queried one after another until one returns a valid IP.
`config.json`
```json
{
"api": {
"<Your-API-Key>": {
"example.com": [ "@", "www", "sub1" ],
"example.org": [ "@", "www", "sub1", "sub2" ]
}
},
"ttl": 3600,
"resolvers": [
"https://ifconfig.me/ip"
"https://me.gandi.net"
],
"log_path": "./log.txt"
}
```
## Features
* Support for arbitrarily many domains and records through a nested data structure.
* Small codebase.
* Logging
## Limitations
* Right now only IPv4 addresses are supported
* Every record is only allowed one A record.
* Extra records (TXT, CNAME and such) will get deleted on update.
## TODO
* Testing
* Command line options controlling: dry-run, config, log, verbosity, force
* Per subdomain TTL
* Nix Flake support with exported config and service options
* Better documentation
* Better logging
* Support IPv6
* Remember other record types (TXT, etc.)
* Detect TTL change and update even when the IP is the same

93
dyn-gandi.py Normal file
View File

@ -0,0 +1,93 @@
import requests, json, re
from datetime import datetime
def loadSettings() -> dict:
# TODO: check integrity
try:
with open('config.json', 'r') as file:
return json.load(file)
except FileNotFoundError:
sample_config = {
'api': {},
'ttl': 3600,
'resolvers': [ 'https://ifconfig.me' ],
'log_path': './log.txt'
}
with open('config.json', 'w') as file:
json.dump(sample_config, file, indent = 2)
return sample_config
# Appends logline at the end of the file file with a timestamp
def log(logline: str, path: str = './log.txt') -> None:
with open(path, 'a') as file:
file.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]{logline}\n")
# Go through resolvers until one retuns an IP
def getCurrentIP(resolvers: list[str], log: callable) -> None | str:
for resolver in resolvers:
response = requests.request('GET', resolver)
if response.status_code == 200:
current_ip = response.text.strip()
is_ipv4 = re.search('^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\\b){4}$', current_ip) is not None
if is_ipv4:
log(f"[OK][{resolver}] Current IP: '{current_ip}'")
return current_ip # Break if we have found our IP
else:
log(f"[ERROR][{resolver}] '{current_ip}' is not IPv4")
else:
log(f"[ERROR][{resolver}][{response.status_code}] {response.text}")
return None
# Returns the records that need to be renewed
# TODO: Also detect TTL change
def checkRecords(api: dict, current_ip: str, log: callable) -> dict:
records_to_renew = api
for key, domains in api.items():
headers = { 'authorization': f"Apikey {key}" }
for domain, records in domains.items():
response = requests.request('GET', f"https://api.gandi.net/v5/livedns/domains/{domain}/records?rrset_type=A", headers = headers)
if response.status_code == 200:
records_to_renew[key][domain] = list(set(records_to_renew[key][domain]) - set(record['rrset_name'] for record in (filter(lambda x: current_ip in x['rrset_values'], json.loads(response.text)))))
else:
log(f"[ERROR][{domain}][{response.status_code}] {response.text}")
return records_to_renew
# Updates the records and reates them if they don't exist
def renewRecords(records, current_ip, log, ttl = 3600):
payload = json.dumps({
'items': [{
'rrset_type': 'A',
'rrset_values': [ current_ip ],
'rrset_ttl': ttl
}]
})
for key, domains in records.items():
headers = {
'authorization': f"Apikey {key}",
'content-type': 'application/json',
}
for domain, records in domains.items():
for record in records:
response = requests.request('PUT', f"https://api.gandi.net/v5/livedns/domains/{domain}/records/{record}", data = payload, headers = headers)
if response.status_code == 201:
log(f"[OK][{domain}][{record}] Renewed with IP '{current_ip}'")
else:
log(f"[ERROR][{domain}][{record}][{response.status_code}] {response.text}")
def main():
settings = loadSettings()
log_path = settings['log_path']
ttl = settings['ttl']
log = lambda x: log(x, path = log_path)
current_ip = getCurrentIP(settings['resolvers'], log)
records_to_renew = checkRecords(settings['api'], current_ip, log)
renewRecords(records_to_renew, current_ip, log, ttl = ttl)
if __name__ == '__main__':
main()