initial commit
This commit is contained in:
commit
7e8356a580
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
config.json
|
||||||
|
log.txt
|
60
README.md
Normal file
60
README.md
Normal 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
93
dyn-gandi.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user