Compare commits

...

50 Commits
flake ... main

Author SHA1 Message Date
310d4bef8b
updated README 2024-09-01 20:13:51 +02:00
fc0d163192
updated to use gandi PATs
Reason:
API Keys are deprecated
2024-09-01 14:58:24 +02:00
8e16fdebfd
it does only make sense to run the service when internet is available 2024-07-27 19:31:13 +02:00
f04316b277
wait for active internet connection 2024-07-27 18:31:19 +02:00
336d3ecf21
removed mypy 2024-05-23 18:57:41 +02:00
11baee6eba
updated inputs 2024-05-23 18:08:56 +02:00
be119a0416
updated python dependencies 2024-05-23 18:08:09 +02:00
334d806ea0
updated dependency management and dev environment 2024-01-02 15:12:51 +01:00
07cd2fc2f9
formatting 2024-01-01 00:45:52 +01:00
ff11a17ff1
changed formatter line 2024-01-01 00:45:19 +01:00
2e0b010bcd
updated flake to out of tree poetry 2024-01-01 00:22:19 +01:00
6e44ec4935
renamed source directory 2023-10-13 14:58:58 +02:00
cfeb253cac
Documentation and fixed a bug with the log 2023-10-13 14:09:03 +02:00
23214ff78f
deleted unused file 2023-10-13 13:57:49 +02:00
a72ccd4b26
Better error message 2023-10-13 13:53:54 +02:00
f66c489fc6
changed gitignore 2023-10-13 13:51:12 +02:00
115aa579ba
changed binary name 2023-10-13 13:50:05 +02:00
7b4a7c6f6d
Log format 2023-10-13 13:39:16 +02:00
b56750c27e
Began refactoring
Changes
* Added documentation
* More types
* Argparse command-line interface
* A '--dry-run' option

Tests
* Passes mypy
* Build with nix
2023-10-05 14:46:06 +02:00
ea99231f03 added type annotation. passed mypy 2023-08-15 23:01:09 +02:00
7eb2ab7fe3 moved config insto nix store for home manager module 2023-08-15 22:13:10 +02:00
84f4b11b9c added config file to the nix store 2023-08-15 21:51:59 +02:00
912b405511 fixed typo 2023-08-15 21:45:59 +02:00
307a8e9819 added config command line option 2023-08-15 21:45:47 +02:00
e6d4878636 changed order 2023-08-13 16:41:43 +02:00
ea719f3994 made nicer 2023-08-13 16:05:59 +02:00
671154c5c2 changed URL 2023-08-13 15:09:42 +02:00
be54a045f3 updated README to reflect current state of the project 2023-08-13 15:06:57 +02:00
f7d5a7d59f changed api call to not overwrite records 2023-08-13 14:49:49 +02:00
993c960aea removed default argument 2023-08-13 14:15:58 +02:00
63fcfdaf3a added typing to dev shell 2023-08-13 14:01:44 +02:00
83c79df6b9 added .mypy_cache 2023-08-13 14:00:59 +02:00
b3a5e6ce46 modules now working 2023-08-13 14:00:02 +02:00
b5ab25940a finalized code and added typing 2023-08-13 13:58:30 +02:00
9754ea32a6 enabled nix module 2023-08-04 23:25:24 +02:00
fcd9326cf9 updated nix modules 2023-08-04 23:10:47 +02:00
e4f94e04a6 formatting, minor tweaks, fixed txpo 2023-08-03 23:14:04 +02:00
67e4afd0bf added direnv support 2023-08-03 14:22:29 +02:00
b9960c46a3 added type annotation 2023-08-02 20:48:06 +02:00
e732a3457d changed path to a standard config path 2023-08-02 20:47:28 +02:00
99d387c692 made more concise 2023-08-02 20:35:08 +02:00
91e4b5f73a added default path 2023-08-02 20:34:16 +02:00
28e7a2855e updated modules in flake 2023-08-01 21:59:54 +02:00
12a4ab7ae7 added support for keys in external files 2023-08-01 21:58:56 +02:00
db71bf8452 fixed typo 2023-08-01 21:12:03 +02:00
b49f8a6a03 used formatter 2023-07-27 13:34:55 +02:00
28901f25f1 added formatter 2023-07-27 13:34:32 +02:00
2511d6ae4d used more elegant library features 2023-07-26 22:31:05 +02:00
8beb0a9fb3 deleted comment 2023-07-26 22:28:14 +02:00
263c6f0a10 working flake 2023-07-26 22:27:02 +02:00
11 changed files with 754 additions and 128 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

12
.gitignore vendored
View File

@ -1,2 +1,10 @@
config.json /result
log.txt
__pycache__/
/.vscode/
/.direnv/
/.mypy_cache/
/config.json
/log.txt
/api.key

105
README.md
View File

@ -1,60 +1,111 @@
# Dyn-Gandi # Dyn-Gandi
A DNS record updater for [Gandi's LiveDNS](https://api.gandi.net/docs/livedns/) API. 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). This script is heavily inspired by [dyn-gandi](https://github.com/Danamir/dyn-gandi).
## How it works ## 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 This script determines the the current IP address by querying the resolvers defined in the config file.
The current implementation only allows for one entry per subdomain. It then queries the subdomains' A records off of Gandi and compares their IP addresses to the current IP address.
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. Should the IP address of a subdomain's A record not match your current IP address it will be updated. The subdomain's A record will be created should it not already exist.
## How to use ## Notes
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. Every invocation of the script causes at least 1 request to a resolver specified and 1 API call to Gandi per domain.
Next, enter the API key into the configuration file and assign domains to it. Updating a subdomain's A record is 1 API request per subdomain, even if they share the same domain.
The domains are keys to a list of subdomain for a given domain you wish to monitor. Resolvers are queried in the order specified until one returns a valid IP address.
The below example is complete and should explain itself. It is also possible to define a path to a file with the API key written in it. This is good for environments where the config file has to be shared like in a nix project.
Resolvers are queried one after another until one returns a valid IP.
## Usage
First, get your Personal Access Token (PAT) from https://account.gandi.net/en/users/USER/security where `USER` is your Gandi username.
The token need the following permissions:
* **Manage domain name technical configurations**
The script looks for a config file at `$HOME/.config/dyn-gandi/config.log` or `/etc/dyn-gandi.conf` in that order. So create a file at one of these locations according to the schema below.
`config.json`
```json ```json
{ {
"api": { "api": {
"<Your-API-Key>": { "<Your-PAT>": {
"example.com": [ "@", "www", "sub1" ], "example.com": [ "@", "www", "sub1" ],
"example.org": [ "@", "www", "sub1", "sub2" ] "example.org": [ "@", "www", "sub1", "sub2" ]
},
"/path/to/a/file/containing/api_key": {
"example.at": [ "sub1" ],
"example.au": [ "sub1", "sub2" ]
} }
}, },
"ttl": 3600,
"resolvers": [ "resolvers": [
"https://ifconfig.me/ip" "https://ifconfig.me/ip",
"https://me.gandi.net" "https://me.gandi.net"
], ],
"ttl": 3600,
"log_path": "./log.txt" "log_path": "./log.txt"
} }
``` ```
## Nix
Add this to the modules.
```nix
inputs = {
dyn-gandi.url = "git+https://git.krsnik.at/Kristian/dyn-gandi";
};
outputs = {
self,
nixpkgs,
dyn-gandi
}: {
...
modules = [
dyn-gandi.nixosModules.default
{
dyn-gandi.enable = true;
dyn-gandi.timer = 300;
dyn-gandi.settings = {
api = {
"/path/to/a/file/containing/api_key" = {
"example.com" = ["@" "www"];
};
};
resolvers = [
"https://ifconfig.me/ip"
"https://me.gandi.net"
];
ttl = 3600;
log_path = "/path/to/log/file";
};
}
...
];
...
}
```
Use `dyn-gandi.nixosModules.default` for a NixOs module and `dyn-gandi.homeManagerModules.default` for home-manager
`dyn-gandi.timer` specifies a timer in seconds when the script should be repeated.
## Features ## Features
* Support for arbitrarily many domains and records through a nested data structure.
* Small codebase. * Support for arbitrarily many domains and subdomains through a nested data structure.
* Small codebase
* Logging * Logging
* NixOS and home-manager modules
## Limitations ## Limitations
* Right now only IPv4 addresses are supported
* Every record is only allowed one A record. * Only IPv4 addresses are supported
* Extra records (TXT, CNAME and such) will get deleted on update.
## TODO ## TODO
* Testing * Testing
* Command line options controlling: dry-run, config, log, verbosity, force * Command line options controlling: dry-run, config, log, verbosity, force
* Support IPv6
* Per subdomain TTL * Per subdomain TTL
* Nix Flake support with exported config and service options
* Better documentation * Better documentation
* Better logging * Better logging
* Support IPv6
* Remember other record types (TXT, etc.)
* Detect TTL change and update even when the IP is the same

View File

@ -1,93 +0,0 @@
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()

141
flake.lock Normal file
View File

@ -0,0 +1,141 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix-lib",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716330097,
"narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix-lib": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_2",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1715251496,
"narHash": "sha256-vRBfJCKvJtu5sYev56XStirA3lAOPv0EkoEV2Nfc+tc=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "291a863e866972f356967d0a270b259f46bf987f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"poetry2nix-lib": "poetry2nix-lib"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"poetry2nix-lib",
"nixpkgs"
]
},
"locked": {
"lastModified": 1714058656,
"narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,11 +1,52 @@
{ {
description = "A very basic flake"; description = "A program to update an A record with the Gandi API v5.";
outputs = { self, nixpkgs }: { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix-lib = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello; outputs = {
self,
nixpkgs,
poetry2nix-lib,
} @ inputs: let
supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
poetry2nix = forAllSystems (system: poetry2nix-lib.lib.mkPoetry2Nix {pkgs = pkgs.${system};});
in {
# `nix build`
packages = forAllSystems (system: {
default = poetry2nix.${system}.mkPoetryApplication {
projectDir = self;
};
});
packages.x86_64-linux.default = self.packages.x86_64-linux.hello; # `nix fmt`
formatter = forAllSystems (system: pkgs.${system}.alejandra);
# `nix develop`
devShells = forAllSystems (system: {
default = let
poetryEnv = poetry2nix.${system}.mkPoetryEnv {projectDir = self;};
in
pkgs.${system}.mkShellNoCC {
packages = with pkgs.${system};
[
poetry
]
++ [poetryEnv];
};
});
# TODO Home Manager Module
# homeManagerModules.default = import ./nix/hm-module.nix self;
# NixOS Module
nixosModules.default = import ./nix/module.nix inputs;
}; };
} }

64
nix/module.nix Normal file
View File

@ -0,0 +1,64 @@
inputs: {
config,
lib,
pkgs,
system,
...
}: let
cfg = config.dyn-gandi;
package = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default;
inherit (lib) mkIf mkEnableOption mkOption types;
format = pkgs.formats.json {};
configFile = format.generate "config.json" cfg.settings;
in {
options.dyn-gandi = {
enable = mkEnableOption "dyn-gandi";
timer = lib.mkOption {
type = types.nullOr types.int;
default = null;
description = lib.mdDoc ''
The time intervall in seconds the script should be repeated.
'';
};
settings = mkOption {
type = with types; let
valueType = nullOr (oneOf [
# TODO: restrict type to actual config file structure
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
]);
in
valueType;
default = throw "Please specify dyn-gandi.settings";
};
};
config = mkIf cfg.enable {
environment.systemPackages = [package];
systemd.services.dyn-gandi = mkIf (cfg.timer != null) {
script = "${package}/bin/dyn-gandi --config ${configFile}";
requires = ["network-online.target"];
serviceConfig = {
Type = "oneshot";
};
};
systemd.timers.dyn-gandi = mkIf (cfg.timer != null) {
wantedBy = ["timers.target"];
timerConfig = {
OnBootSec = "0s";
OnUnitActiveSec = "${toString cfg.timer}s";
Unit = "dyn-gandi.service";
};
};
};
}

179
poetry.lock generated Normal file
View File

@ -0,0 +1,179 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "requests"
version = "2.32.2"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
{file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "types-requests"
version = "2.32.0.20240523"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-requests-2.32.0.20240523.tar.gz", hash = "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57"},
{file = "types_requests-2.32.0.20240523-py3-none-any.whl", hash = "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"},
]
[package.dependencies]
urllib3 = ">=2"
[[package]]
name = "urllib3"
version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "0c18c1520626bc79b003d201df4a1688af4e3cfc4805627505de0ff569c9102b"

21
pyproject.toml Normal file
View File

@ -0,0 +1,21 @@
[tool.poetry]
name = "dyn-gandi"
version = "0.1.0"
description = "A DNS updater"
authors = ["Kristian Krsnik <git@krsnik.at>"]
readme = "README.md"
packages = [{ include = "src" }]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.29.0"
[tool.poetry.scripts]
dyn-gandi = "src.main:main"
[tool.poetry.group.dev.dependencies]
types-requests = "^2.31.0.20231231"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

0
src/__init__.py Normal file
View File

213
src/main.py Normal file
View File

@ -0,0 +1,213 @@
import os
import re
import json
import argparse
from datetime import datetime
from typing import Callable, Any
import requests
def loadSettings(path: str | None) -> dict:
'''
Load the settings file.
`path` Path to a settings file.
By default the program will look for `$HOME/.config/dyn-gandi/config.json` and then `/etc/dyn-gandi.json`.
'''
if path is None:
path = f"{os.path.expanduser('~')}/.config/dyn-gandi/config.json"
if not os.path.exists(path):
path = '/etc/dyn-gandi.json'
if not os.path.exists(path):
print(
f"[ERROR] No config file found at '~/.config/dyn-gandi/config.json' or '/etc/dyn-gandi.json'")
quit()
if not os.path.exists(path):
print(f"[ERROR] '{path}' does not exist.")
quit()
# TODO check integrity of the config file
with open(path, 'r') as file:
# Load as dictionary
config = json.load(file)
# API Key entries
keys = config['api'].keys()
# This will build up a copy of the API section with the API keys instead of filenames.
mask = dict()
# If the API key is a file the use the contents as a new key.
for key in keys:
api_key = key
if os.path.exists(key):
with open(key, 'r') as file:
api_key = file.readline().strip()
mask[key] = api_key
config['api'] = {mask[key]: value for key,
value in config['api'].items()}
return config
def log(logline: str, path: str = './log.txt', stdout: bool = False) -> None:
'''
Appends `logline` at the end of the file file with a timestamp.
`logline` The line to log.
'path' Path to file where to log.
`stdout` Wether to print to stdout instead of a file.
'''
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]{logline}"
if stdout:
print(line)
else:
with open(path, 'a') as file:
file.write(line + '\n')
def getCurrentIP(resolvers: list[str], log: Callable[[str], Any]) -> str | None:
'''
Go through resolvers until one returns an IP.
`resolvers` A list of IP resolvers.
`log` A function which takes a string and logs it.
'''
for resolver in resolvers:
response = requests.get(resolver)
if response.ok:
current_ip = response.text.strip()
# It suffices to check whether the search is not None since the regex matches from beginning to end.
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
def checkRecords(api: dict[str, dict[str, list[str]]], current_ip: str, log: Callable[[str], Any]) -> dict[str, dict[str, list[str]]]:
'''
Return records that need to be renewed.
`api` The API section of the settings file.
`current_ip` The current IP address.
`log` A function that takes in a string and logs it.
'''
# TODO: Detect TTL change
records_to_renew = api
for key, domains in api.items():
headers = {'Authorization': f"Bearer {key}"}
for domain, _ in domains.items():
response = requests.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'], response.json()))))
if (records := records_to_renew[key][domain]) != []:
log(f"[INFO][{','.join(records)}] Records need to be renewed")
else:
log(f"[ERROR][{domain}][{response.status_code}] {response.json()}")
return records_to_renew
def renewRecords(api: dict[str, dict[str, list[str]]], current_ip: str, log: Callable[[str], Any], ttl: int = 300) -> None:
'''
Update records and create them, if they don't exist.
`api` The API section of the settings file.
`current_ip` The current IP address.
`log` A function that takes a string and logs it.
`ttl` How long a record should be cached.
'''
payload = json.dumps({
'rrset_values': [current_ip],
'rrset_ttl': ttl
})
for api_key, domains in api.items():
headers = {
'Authorization': f"Bearer {api_key}",
'content-type': 'application/json',
}
for domain, records in domains.items():
for record in records:
response = requests.put(
f"https://api.gandi.net/v5/livedns/domains/{domain}/records/{record}/A", 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.json()}")
def parseArgs(args: list[str]) -> argparse.Namespace:
'''
Parse command-line arguments.
`args` A list of user-supplied command line arguments.
'''
parser = argparse.ArgumentParser(
description='A program that renews A records via the Gandi API v5.')
parser.add_argument('--config', type=str, default=None,
help='Path to a config file.')
parser.add_argument('--dry-run', action='store_true',
help='Just perform a dry-run.')
return parser.parse_args(args)
def main():
# Parse user-supplied command-line arguments.
args = parseArgs(os.sys.argv[1:])
# Read in the settings file.
config = loadSettings(args.config)
def logger(x: str) -> Any: return log(x, config['log_path'], args.dry_run)
current_ip = getCurrentIP(config['resolvers'], logger)
if current_ip is not None:
records_to_renew = checkRecords(config['api'], current_ip, logger)
# Do not perform an actual record change during a dry-run.
if not args.dry_run:
renewRecords(records_to_renew, current_ip, logger, config['ttl'])
if __name__ == '__main__':
main()