2021-02-27 00:10:11 +01:00
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import json
import ovh
import sys
import requests , time
'''
API credentials
can be provided in variety of ways :
( see https : / / github . com / ovh / python - ovh #configuration)
- explicitly here :
'''
#client = ovh.Client(
# endpoint = "ovh-eu",
# application_key = "XXXXXXXXXXXXXXXX",
# application_secret = "YYYYYYYYYYYYYYYY",
# consumer_key = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
# )
'''
- or in the ENVIRONMENT
- or in a ovh . conf file
2021-02-27 13:11:26 +01:00
in which cases we can call with no argument ( the ovh module gets the credentials on its own ) :
2021-02-27 00:10:11 +01:00
'''
2021-02-28 11:17:54 +01:00
client = ovh . Client ( )
# Should missing IP address being considered as error (Depend on the ISP, internet box settings...)?
# In case a required IP cannot be obtained, the script will send an email and stop without updating anything.
# If, for any reason, IPv4 or IPv6 address cannot be obtained and if it is not protected by this list, the corresponding records will be deleted for all hosts.
ip_versions_required = [ 4 ] # MUST not be empty. Can be [4],[6] or [4,6]
2021-02-27 00:10:11 +01:00
2021-02-27 13:11:26 +01:00
default_ttl = 600 # seconds
2021-02-27 00:10:11 +01:00
# ttl = how long will a DNS server cache the value before checking it at the Registrar. Longer value yields faster name resolution most of the time, but less frequent updates
2021-02-28 11:17:54 +01:00
2021-02-27 00:10:11 +01:00
# list of hosts (=subdomain.domain.tld) to update, each a dictionnary with at least "domain" and "subdomain" defined
hosts = [
2021-02-27 13:11:26 +01:00
{
2021-02-27 00:10:11 +01:00
" domain " : " mydomain.tld " , # Required
" subdomain " : " www " , # Required. Explicit subdomain or empty string "" (for @) or "*" for wildcard
#"ipv6": any_value_except_False # Optional : maintain corresponding record, when possible
" ipv4 " : False , #explicitly disable modifiying ipv4 (A) records, even if public IPV4 exists (a possibly erroneous record would be left as-is)
#"ttl": 60 # optional : if 'ttl' in specified in host, overrides the global default value
} ,
2021-02-27 13:11:26 +01:00
{
2021-02-27 00:10:11 +01:00
" domain " : " otherdomain.tld " ,
" subdomain " : " "
# 'ipv4' and 'ipv6' are not listed : automatically maintain any/both records, according to availability
}
]
checkDNS_interval_hrs = 12.1 # when the saved IP addresses are old, check the DNS record, even if the addresses did not change
2021-02-27 13:11:26 +01:00
#save last known address in local file.
current_ip_file = " /tmp/current_ip.json "
2021-02-27 00:10:11 +01:00
2021-02-27 13:11:26 +01:00
def send_email ( msg , sender = ' no_reply@mydomain.com ' , receiver = ' admin@mydomain.com ' ) :
2021-02-27 12:34:08 +01:00
import smtplib
try :
smtpObj = smtplib . SMTP ( ' localhost ' )
smtpObj . sendmail ( sender , receiver ,
2021-02-28 11:17:54 +01:00
" From: {} \n To: {} \n Subject: DNS update problem \n \n The ovh-dns-updater.py script reports: \n {} \n " . format ( sender , receiver , msg )
2021-02-27 13:11:26 +01:00
)
2021-02-27 12:34:08 +01:00
except smtplib . SMTPException :
print ( timestamp ( ) , " : Error unable to send email " )
2021-02-27 00:10:11 +01:00
def get_current_ip ( v = 4 ) :
2021-02-28 22:07:06 +01:00
#url = 'https://api6.ipify.org' if v == 6 else 'https://api.ipify.org' #frequent error on ipv6 https://github.com/rdegges/ipify-api/issues/54
url = ' https://v {} .ident.me ' . format ( v )
2021-02-27 13:11:26 +01:00
try :
2021-02-27 00:10:11 +01:00
r = requests . get ( url , timeout = 5.0 )
except requests . exceptions . RequestException as e :
2021-02-27 12:34:08 +01:00
#print("failed getting ipv{} address because {} occurred".format(v, str(e)))
2021-02-28 11:17:54 +01:00
if v in ip_versions_required :
message = " {} : Cannot get required IPv {} because {} occurred. Failing " . format ( timestamp ( ) , v , e )
print ( message )
send_email ( message )
quit ( )
else :
return False
2021-02-28 11:39:15 +01:00
if r . status_code == requests . codes . ok :
2021-02-28 11:17:54 +01:00
return r . text
elif v in ip_versions_required :
2021-02-28 22:07:06 +01:00
message = " {} : Cannot get required IPv {} : requests.get returned status_code {} . Failing " . format ( timestamp ( ) , v , r . status_code )
print ( message )
send_email ( message )
quit ( )
2021-02-28 11:17:54 +01:00
else :
2021-02-27 00:10:11 +01:00
return False
2021-02-27 13:11:26 +01:00
2021-02-27 12:34:08 +01:00
def timestamp ( ) :
return time . asctime ( time . localtime ( time . time ( ) ) )
2021-02-27 00:10:11 +01:00
def update_record ( domain , subdomain , new_ip , _ttl = 600 ) :
2021-02-27 12:34:08 +01:00
#Update the (A or AAAA) record with the provided IP
2021-02-27 00:10:11 +01:00
2021-04-10 16:03:25 +02:00
global records_changed
2021-02-27 00:10:11 +01:00
typ = ' AAAA ' if " : " in new_ip else ' A '
2021-02-27 12:34:08 +01:00
#print("checking record {} for {}.{}".format(typ,subdomain,domain))
2021-02-27 00:10:11 +01:00
path = " /domain/zone/ {} /record " . format ( domain )
result = client . get ( path ,
fieldType = typ ,
subDomain = subdomain
)
if len ( result ) != 1 :
2021-02-27 12:34:08 +01:00
#creating NEW record
2021-02-27 00:10:11 +01:00
result = client . post ( path ,
fieldType = typ ,
subDomain = subdomain ,
target = new_ip ,
ttl = _ttl
)
client . post ( ' /domain/zone/ {} /refresh ' . format ( domain ) )
result = client . get ( path ,
fieldType = typ ,
subDomain = subdomain
)
record_id = result [ 0 ]
2021-02-27 12:34:08 +01:00
records_changed + = 1
print ( " {} : ### created new record {} for {} . {} " . format ( timestamp ( ) , typ , subdomain , domain ) )
2021-02-27 00:10:11 +01:00
else :
# record exists
record_id = result [ 0 ]
path = " /domain/zone/ {} /record/ {} " . format ( domain , record_id )
result = client . get ( path )
oldip = result [ ' target ' ]
2021-02-27 12:34:08 +01:00
#print('record exists, with ip :',oldip)
2021-02-27 00:10:11 +01:00
if oldip == new_ip :
2021-02-27 12:34:08 +01:00
#print('nothing to do')
2021-02-27 00:10:11 +01:00
return
else :
2021-02-27 12:34:08 +01:00
#print('updating to ', new_ip)
2021-02-27 13:11:26 +01:00
result = client . put ( path ,
subDomain = subdomain ,
target = new_ip ,
2021-02-27 00:10:11 +01:00
ttl = _ttl
)
client . post ( ' /domain/zone/ {} /refresh ' . format ( domain ) )
2021-02-27 12:34:08 +01:00
records_changed + = 1
2021-02-27 00:10:11 +01:00
#checking changes
result = client . get ( " /domain/zone/ {} /record/ {} " . format ( domain , record_id ) )
if new_ip != result [ ' target ' ] :
2021-02-27 12:34:08 +01:00
records_changed - = 1
2021-02-27 00:10:11 +01:00
raise Exception ( " Error updating {} . {} with {} " . format ( subdomain , domain , new_ip ) )
2021-02-27 13:11:26 +01:00
2021-02-27 00:10:11 +01:00
def delete_record ( domain , subdomain , typ ) :
"""
2021-02-27 13:11:26 +01:00
if it exists , delete an A or AAAA record
2021-02-27 00:10:11 +01:00
( because the corresponding IP is not available )
"""
2021-02-27 12:34:08 +01:00
#print("checking record {} for {}.{}".format(typ,subdomain,domain))
2021-04-10 16:03:25 +02:00
global records_changed
2021-02-27 00:10:11 +01:00
result = client . get ( " /domain/zone/ {} /record " . format ( domain ) ,
fieldType = typ ,
subDomain = subdomain
)
if len ( result ) == 1 :
# record exists, delete it
record_id = result [ 0 ]
2021-02-27 12:34:08 +01:00
print ( " {} : ### deleting record {} for {} . {} " . format ( timestamp ( ) , typ , subdomain , domain ) )
2021-02-27 00:10:11 +01:00
client . delete ( " /domain/zone/ {} /record/ {} " . format ( domain , record_id ) )
client . post ( ' /domain/zone/ {} /refresh ' . format ( domain ) )
2021-02-27 12:34:08 +01:00
records_changed + = 1
2021-02-27 00:10:11 +01:00
current_ipv4 = get_current_ip ( 4 )
current_ipv6 = get_current_ip ( 6 )
2021-02-27 12:34:08 +01:00
#print('current ips: {} ; {}'.format(current_ipv4, current_ipv6))
2021-02-27 00:10:11 +01:00
2021-02-28 11:17:54 +01:00
#reload saved values & compare
try :
with open ( current_ip_file , ' r ' ) as f :
old_time , old_ipv4 , old_ipv6 = json . load ( f )
2021-02-27 00:10:11 +01:00
need_update = ( old_ipv4 != current_ipv4 ) or ( old_ipv6 != current_ipv6 ) or ( ( old_time - time . time ( ) ) > 3600.0 * checkDNS_interval_hrs )
2021-02-28 11:17:54 +01:00
except IOError :
#print("No old ips recorded")
2021-02-27 13:11:26 +01:00
need_update = True
2021-02-28 11:17:54 +01:00
if need_update :
records_changed = 0
try :
for host in hosts :
domain = host [ " domain " ]
subdomain = host [ " subdomain " ]
if ( ' ipv4 ' not in host ) or ( host [ ' ipv4 ' ] != False ) :
if current_ipv4 :
ttl = default_ttl if ( ' ttl ' not in host ) else host [ ' ttl ' ]
update_record ( domain , subdomain , current_ipv4 , _ttl = ttl )
else :
delete_record ( domain , subdomain , ' A ' )
else :
#print("Not touching A record for {}.{}, as instructed".format(subdomain, domain))
pass
if ( ' ipv6 ' not in host ) or ( host [ ' ipv6 ' ] != False ) :
if current_ipv6 :
ttl = default_ttl if ( ' ttl ' not in host ) else host [ ' ttl ' ]
update_record ( domain , subdomain , current_ipv6 , _ttl = ttl )
else :
delete_record ( domain , subdomain , ' AAAA ' )
else :
#print("Not touching AAAA record for {}.{}, as instructed".format(subdomain, domain))
pass
#all hosts records have been updated without errors, log change and save current addresses
print ( " {} : new addresses {} ; {} -- {} records updates " . format ( timestamp ( ) , current_ipv4 , current_ipv6 , records_changed ) )
with open ( current_ip_file , ' w ' ) as f :
json . dump ( [ time . time ( ) , current_ipv4 , current_ipv6 ] , f )
except Exception as e : #some error occured (API down, keys expired...?),
msg = " {} : ### error {} while updating records!! {} records updated with new addresses {} ; {} " . format ( timestamp ( ) , str ( e ) , records_changed , current_ipv4 , current_ipv6 )
print ( msg )
send_email ( msg )
# not saving new addresses, so that update is attempted again.
2021-02-27 00:10:11 +01:00
else :
2021-02-28 11:17:54 +01:00
#print("nothing to do!")
pass
2021-02-27 12:34:08 +01:00
2021-02-27 00:10:11 +01:00