· 6 years ago · Mar 12, 2020, 12:12 PM
1"""
2
3Copyright (c) 2017 Arnaud Levaufre <arnaud@levaufre.name>
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
22
23"""
24
25import argparse
26import json
27import logging
28import os
29import urllib.request
30import time
31
32
33IPV4_PROVIDER_URL = "https://api.ipify.org"
34IPV6_PROVIDER_URL = "https://api6.ipify.org"
35GANDI_API_URL = "https://dns.api.gandi.net/api/v5"
36LOG_FORMAT = "[%(asctime)s][%(name)s][%(levelname)s] %(message)s"
37
38
39logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
40logger = logging.getLogger(__name__)
41
42
43class GandiAPI:
44 def __init__(self, url, key):
45 self.url = url
46 self.key = key
47
48 def get_domain_record_by_name(self, fqdn, name, rtype="A"):
49 try:
50 request = urllib.request.Request("{}/domains/{}/records/{}/{}".format(self.url,fqdn,name,rtype))
51 request.add_header("X-Api-Key", self.key)
52 with urllib.request.urlopen(request) as response:
53 return json.loads(response.read().decode())
54 except urllib.error.HTTPError:
55 return None
56
57 def update_records(self, fqdn, record_names, current_ip, ttl=10800, rtype="A"):
58
59 for name in record_names:
60 record = self.get_domain_record_by_name(fqdn, name, rtype=rtype)
61 if record is not None and current_ip in record['rrset_values']:
62 logger.info("Record %s for %s.%s is up to date.",rtype, name, fqdn)
63 else:
64 request = urllib.request.Request(
65 "{}/domains/{}/records/{}/{}".format(self.url,fqdn,name,rtype),
66 method="POST" if record is None else "PUT",
67 headers={"Content-Type": "application/json", "X-Api-Key": self.key},
68 data=json.dumps({"rrset_ttl": ttl,"rrset_values": [current_ip],}).encode()
69 )
70 with urllib.request.urlopen(request) as response:
71 logger.debug(json.loads(response.read().decode()))
72 logger.info("Record %s for %s.%s is set to %s.",rtype, name, fqdn, current_ip)
73
74
75def get_current_ip(provider_url):
76 with urllib.request.urlopen(provider_url) as response:
77 return response.read().decode()
78
79
80def main():
81
82 parser = argparse.ArgumentParser(
83 description=
84 """"
85 Keep your gandi DNS records up to date with your current IP
86 """
87 )
88 parser.add_argument('key', type=str, help="Gandi API key or path to a file containing the key.")
89 parser.add_argument('zone', type=str, help="Zone to update")
90 parser.add_argument('record', type=str, nargs='+', help="Records to update")
91 parser.add_argument("--ttl", type=int, default=10800, help="Set a custom ttl (in second)")
92 parser.add_argument("--noipv4", action="store_true", help="Do not set 'A' records to current ipv4")
93 parser.add_argument("--noipv6", action="store_true", help="Do not set 'AAAA' records to current ipv6")
94 args = parser.parse_args()
95
96 time.sleep(120)
97 logger.info('Gandi record update started.')
98
99 if os.path.isfile(args.key):
100 with open(args.key) as fle:
101 gandi_api_key = fle.read().strip()
102 else:
103 gandi_api_key = args.key
104
105 api = GandiAPI(GANDI_API_URL, gandi_api_key)
106
107 if not args.noipv4:
108 current_ipv4 = get_current_ip(IPV4_PROVIDER_URL)
109 api.update_records(args.zone, args.record, current_ipv4, ttl=args.ttl)
110
111 if not args.noipv6:
112 current_ipv6 = get_current_ip(IPV6_PROVIDER_URL)
113 api.update_records(args.zone, args.record, current_ipv6, ttl=args.ttl, rtype="AAAA")
114
115
116if __name__ == "__main__":
117 main()