22
33import argparse
44import logging as log # for verbose output
5+ import os
56import socket # to get default hostname
67import sys
78
89import CloudFlare
910import tldextract
11+ from CloudFlare .exceptions import CloudFlareAPIError
1012
1113from .__about__ import __version__
1214
1315
14- def update (cf_username , cf_key , hostname , ip , proxied = True , ttl = 120 ):
15-
16- log .info ("Updating {} to {}" .format (hostname , ip ))
16+ def update (cfUsername , cfKey , hostname , ip , ttl = None ):
17+ """
18+ Create or update desired DNS record.
19+ Returns Synology-friendly status strings:
20+ https://community.synology.com/enu/forum/17/post/57640?reply=213305
21+ """
22+ log .debug ("Updating {} to {}" .format (hostname , ip ))
1723
1824 # get zone name correctly (from hostname)
19- zone_domain = tldextract .extract (hostname ).registered_domain
20- log .info ("Zone domain of hostname is {}" .format (zone_domain ))
25+ zoneDomain = tldextract .extract (hostname ).registered_domain
26+ log .debug ("Zone domain of hostname is {}" .format (zoneDomain ))
2127
2228 if ':' in ip :
23- ip_address_type = 'AAAA'
29+ ipAddressType = 'AAAA'
2430 else :
25- ip_address_type = 'A'
31+ ipAddressType = 'A'
2632
27- cf = CloudFlare .CloudFlare (email = cf_username , token = cf_key )
33+ cf = CloudFlare .CloudFlare (email = cfUsername , token = cfKey )
2834 # now get the zone id
29- zones = []
3035 try :
31- params = {'name' : zone_domain }
36+ params = {'name' : zoneDomain }
3237 zones = cf .zones .get (params = params )
33- except CloudFlare . exceptions . CloudFlareAPIError as e :
34- log .error ('bad auth - %s' % e )
35- exit ( 1 )
38+ except CloudFlareAPIError as e :
39+ log .error ('Bad auth - %s' % e )
40+ return 'badauth'
3641 except Exception as e :
37- exit ('/zones.get - %s - api call failed' % e )
42+ log .error ('/zones.get - %s - api call failed' % e )
43+ return '911'
3844
3945 if len (zones ) == 0 :
40- log .error ('no host' )
41- exit ( 1 )
46+ log .error ('No host' )
47+ return 'nohost'
4248
4349 if len (zones ) != 1 :
44- exit ('/zones.get - %s - api call returned %d items' % (zone_domain , len (zones )))
50+ log .error ('/zones.get - %s - api call returned %d items' % (zoneDomain , len (zones )))
51+ return 'notfqdn'
4552
4653 zone_id = zones [0 ]['id' ]
47- log .info ("Zone ID is {}" .format (zone_id ))
54+ log .debug ("Zone ID is {}" .format (zone_id ))
4855
49- dns_records = []
5056 try :
51- params = {'name' : hostname , 'match' : 'all' , 'type' : ip_address_type }
57+ params = {'name' : hostname , 'match' : 'all' , 'type' : ipAddressType }
5258 dns_records = cf .zones .dns_records .get (zone_id , params = params )
53- except CloudFlare .exceptions .CloudFlareAPIError as e :
54- exit ('/zones/dns_records %s - %d %s - api call failed' % (hostname , e , e ))
55-
56- updated = False
59+ except CloudFlareAPIError as e :
60+ log .error ('/zones/dns_records %s - %d %s - api call failed' % (hostname , e , e ))
61+ return '911'
62+
63+ desiredRecordData = {
64+ 'name' : hostname ,
65+ 'type' : ipAddressType ,
66+ 'content' : ip
67+ }
68+ if ttl :
69+ desiredRecordData ['ttl' ] = ttl
5770
5871 # update the record - unless it's already correct
59- for dns_record in dns_records :
60- old_ip = dns_record ['content' ]
61- old_ip_type = dns_record ['type' ]
72+ for dnsRecord in dns_records :
73+ oldIp = dnsRecord ['content' ]
74+ oldIpType = dnsRecord ['type' ]
6275
63- if ip_address_type not in ['A' , 'AAAA' ]:
76+ if ipAddressType not in ['A' , 'AAAA' ]:
6477 # we only deal with A / AAAA records
6578 continue
6679
67- if ip_address_type != old_ip_type :
80+ if ipAddressType != oldIpType :
6881 # only update the correct address type (A or AAAA)
6982 # we don't see this becuase of the search params above
70- log .info ('IGNORED: %s %s ; wrong address family' % (hostname , old_ip ))
83+ log .debug ('IGNORED: %s %s ; wrong address family' % (hostname , oldIp ))
7184 continue
7285
73- if ip == old_ip :
74- log .info ('UNCHANGED: %s %s' % (hostname , ip ))
75- updated = True
76- continue
86+ if ip == oldIp :
87+ log .info ('UNCHANGED: %s == %s' % (hostname , ip ))
88+ # nothing to do, record already matches to desired IP
89+ return 'nochg'
7790
7891 # Yes, we need to update this record - we know it's the same address type
92+ dnsRecordId = dnsRecord ['id' ]
7993
80- dns_record_id = dns_record ['id' ]
81- dns_record = {
82- 'name' : hostname ,
83- 'type' : ip_address_type ,
84- 'content' : ip ,
85- 'proxied' : proxied ,
86- 'ttl' : ttl
87- }
88- try :
89- cf .zones .dns_records .put (zone_id , dns_record_id , data = dns_record )
90- except CloudFlare .exceptions .CloudFlareAPIError as e :
91- exit ('/zones.dns_records.put %s - %d %s - api call failed' % (hostname , e , e ))
92- log .info ('UPDATED: %s %s -> %s' % (hostname , old_ip , ip ))
93- updated = True
94-
95- if not updated :
96- # no exsiting dns record to update - so create dns record
97- dns_record = {
98- 'name' : hostname ,
99- 'type' : ip_address_type ,
100- 'content' : ip ,
101- 'ttl' : ttl
102- }
10394 try :
104- cf .zones .dns_records .post (zone_id , data = dns_record )
95+ cf .zones .dns_records .put (zone_id , dnsRecordId , data = desiredRecordData )
10596 except CloudFlare .exceptions .CloudFlareAPIError as e :
106- exit ('/zones.dns_records.post %s - %d %s - api call failed' % (hostname , e , e ))
97+ log .error ('/zones.dns_records.put %s - %d %s - api call failed' % (hostname , e , e ))
98+ return '911'
99+ log .info ('UPDATED: %s %s -> %s' % (hostname , oldIp , ip ))
100+ return 'good'
101+
102+ # no exsiting dns record to update - so create dns record
103+ try :
104+ cf .zones .dns_records .post (zone_id , data = desiredRecordData )
107105 log .info ('CREATED: %s %s' % (hostname , ip ))
106+ return 'good'
107+ except CloudFlare .exceptions .CloudFlareAPIError as e :
108+ log .error ('/zones.dns_records.post %s - %d %s - api call failed' % (hostname , e , e ))
109+ return '911'
108110
109- # reached far enough, all good then (the text is required by Synology)
110- print ('good' )
111111
112112
113+ # reached far enough without genuine return/exception catching, must be an error
114+ # using 'badagent' just because it is unique to other statuses used above
115+ return 'badagent'
116+
117+ def updateRecord (hostname , ip , ttl = None ):
118+ res = update (os .environ ['CF_EMAIL' ], os .environ ['CF_KEY' ], hostname , ip , ttl )
119+ return res in ['good' , 'nochg' ]
120+
113121def main ():
114122 parser = argparse .ArgumentParser (description = 'Update DDNS in Cloudflare.' )
115123 parser .add_argument ('--email' , help = 'Cloudflare account emai' )
@@ -118,31 +126,31 @@ def main():
118126 help = 'Hostname to set IP for' )
119127 parser .add_argument ('--ip' , dest = 'ip' ,
120128 help = 'The IP address' )
129+ parser .add_argument ('--ttl' , type = int , help = 'TTL in seconds' )
121130 parser .add_argument ('--verbose' , dest = 'verbose' , action = 'store_true' )
122131
123132 parser .add_argument ('--version' , action = 'version' ,
124133 version = '%(prog)s {version}' .format (version = __version__ ))
125134
126- parser .set_defaults (hostname = socket .getfqdn ())
135+ parser .set_defaults (hostname = socket .getfqdn (), ttl = None , email = os .environ ['CF_EMAIL' ],
136+ key = os .environ ['CF_KEY' ])
127137
128138 args = parser .parse_args ()
129139
130140 if args .verbose :
131141 log .basicConfig (format = "%(levelname)s: %(message)s" , level = log .DEBUG )
132- log .info ("Verbose output." )
142+ log .debug ("Verbose output." )
133143 else :
134- log .basicConfig (format = "%(levelname)s: %(message)s" )
135-
136- cf_username = args .email
137- cf_key = args .key
138- hostname = args .hostname
139- ip = args .ip
144+ log .basicConfig (format = "%(message)s" , level = log .INFO )
140145
141- update (cf_username , cf_key , hostname , ip )
146+ update (args . email , args . key , args . hostname , args . ip , args . ttl )
142147
143148
144149def syno ():
145- update (sys .argv [1 ], sys .argv [2 ], sys .argv [3 ], sys .argv [4 ])
150+ """
151+ In Synology wrapper, we echo the return value of the "update" for users to see errors:
152+ """
153+ print (update (sys .argv [1 ], sys .argv [2 ], sys .argv [3 ], sys .argv [4 ], 120 ))
146154
147155
148156if __name__ == '__main__' :
0 commit comments