@ -5,521 +5,18 @@
from __future__ import absolute_import , division , print_function , \
unicode_literals
from requests import HTTPError , Session
from operator import itemgetter
import logging
from ..record import Create , Record
from .base import BaseProvider
class PowerDnsBaseProvider ( BaseProvider ) :
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set ( ( ' A ' , ' AAAA ' , ' ALIAS ' , ' CAA ' , ' CNAME ' , ' LOC ' , ' MX ' , ' NAPTR ' ,
' NS ' , ' PTR ' , ' SPF ' , ' SSHFP ' , ' SRV ' , ' TXT ' ) )
TIMEOUT = 5
def __init__ ( self , id , host , api_key , port = 8081 ,
scheme = " http " , timeout = TIMEOUT , * args , * * kwargs ) :
super ( PowerDnsBaseProvider , self ) . __init__ ( id , * args , * * kwargs )
self . host = host
self . port = port
self . scheme = scheme
self . timeout = timeout
self . _powerdns_version = None
sess = Session ( )
sess . headers . update ( { ' X-API-Key ' : api_key } )
self . _sess = sess
def _request ( self , method , path , data = None ) :
self . log . debug ( ' _request: method= %s , path= %s ' , method , path )
url = f ' {self.scheme}://{self.host}:{self.port}/api/v1/servers/ ' \
f ' localhost/{path} ' . rstrip ( ' / ' )
# Strip trailing / from url.
resp = self . _sess . request ( method , url , json = data , timeout = self . timeout )
self . log . debug ( ' _request: status= %d ' , resp . status_code )
resp . raise_for_status ( )
return resp
def _get ( self , path , data = None ) :
return self . _request ( ' GET ' , path , data = data )
def _post ( self , path , data = None ) :
return self . _request ( ' POST ' , path , data = data )
def _patch ( self , path , data = None ) :
return self . _request ( ' PATCH ' , path , data = data )
def _data_for_multiple ( self , rrset ) :
# TODO: geo not supported
return {
' type ' : rrset [ ' type ' ] ,
' values ' : [ r [ ' content ' ] for r in rrset [ ' records ' ] ] ,
' ttl ' : rrset [ ' ttl ' ]
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
_data_for_NS = _data_for_multiple
def _data_for_CAA ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
flags , tag , value = record [ ' content ' ] . split ( ' ' , 2 )
values . append ( {
' flags ' : flags ,
' tag ' : tag ,
' value ' : value [ 1 : - 1 ] ,
} )
return {
' type ' : rrset [ ' type ' ] ,
' values ' : values ,
' ttl ' : rrset [ ' ttl ' ]
}
def _data_for_single ( self , rrset ) :
return {
' type ' : rrset [ ' type ' ] ,
' value ' : rrset [ ' records ' ] [ 0 ] [ ' content ' ] ,
' ttl ' : rrset [ ' ttl ' ]
}
_data_for_ALIAS = _data_for_single
_data_for_CNAME = _data_for_single
_data_for_PTR = _data_for_single
def _data_for_quoted ( self , rrset ) :
return {
' type ' : rrset [ ' type ' ] ,
' values ' : [ r [ ' content ' ] [ 1 : - 1 ] for r in rrset [ ' records ' ] ] ,
' ttl ' : rrset [ ' ttl ' ]
}
_data_for_SPF = _data_for_quoted
_data_for_TXT = _data_for_quoted
def _data_for_LOC ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
lat_degrees , lat_minutes , lat_seconds , lat_direction , \
long_degrees , long_minutes , long_seconds , long_direction , \
altitude , size , precision_horz , precision_vert = \
record [ ' content ' ] . replace ( ' m ' , ' ' ) . split ( ' ' , 11 )
values . append ( {
' lat_degrees ' : int ( lat_degrees ) ,
' lat_minutes ' : int ( lat_minutes ) ,
' lat_seconds ' : float ( lat_seconds ) ,
' lat_direction ' : lat_direction ,
' long_degrees ' : int ( long_degrees ) ,
' long_minutes ' : int ( long_minutes ) ,
' long_seconds ' : float ( long_seconds ) ,
' long_direction ' : long_direction ,
' altitude ' : float ( altitude ) ,
' size ' : float ( size ) ,
' precision_horz ' : float ( precision_horz ) ,
' precision_vert ' : float ( precision_vert ) ,
} )
return {
' ttl ' : rrset [ ' ttl ' ] ,
' type ' : rrset [ ' type ' ] ,
' values ' : values
}
def _data_for_MX ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
preference , exchange = record [ ' content ' ] . split ( ' ' , 1 )
values . append ( {
' preference ' : preference ,
' exchange ' : exchange ,
} )
return {
' type ' : rrset [ ' type ' ] ,
' values ' : values ,
' ttl ' : rrset [ ' ttl ' ]
}
def _data_for_NAPTR ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
order , preference , flags , service , regexp , replacement = \
record [ ' content ' ] . split ( ' ' , 5 )
values . append ( {
' order ' : order ,
' preference ' : preference ,
' flags ' : flags [ 1 : - 1 ] ,
' service ' : service [ 1 : - 1 ] ,
' regexp ' : regexp [ 1 : - 1 ] ,
' replacement ' : replacement ,
} )
return {
' type ' : rrset [ ' type ' ] ,
' values ' : values ,
' ttl ' : rrset [ ' ttl ' ]
}
def _data_for_SSHFP ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
algorithm , fingerprint_type , fingerprint = \
record [ ' content ' ] . split ( ' ' , 2 )
values . append ( {
' algorithm ' : algorithm ,
' fingerprint_type ' : fingerprint_type ,
' fingerprint ' : fingerprint ,
} )
return {
' type ' : rrset [ ' type ' ] ,
' values ' : values ,
' ttl ' : rrset [ ' ttl ' ]
}
def _data_for_SRV ( self , rrset ) :
values = [ ]
for record in rrset [ ' records ' ] :
priority , weight , port , target = \
record [ ' content ' ] . split ( ' ' , 3 )
values . append ( {
' priority ' : priority ,
' weight ' : weight ,
' port ' : port ,
' target ' : target ,
} )
return {
' type ' : rrset [ ' type ' ] ,
' values ' : values ,
' ttl ' : rrset [ ' ttl ' ]
}
@property
def powerdns_version ( self ) :
if self . _powerdns_version is None :
try :
resp = self . _get ( ' ' )
except HTTPError as e :
if e . response . status_code == 401 :
# Nicer error message for auth problems
raise Exception ( f ' PowerDNS unauthorized host={self.host} ' )
raise
version = resp . json ( ) [ ' version ' ]
self . log . debug ( ' powerdns_version: got version %s from server ' ,
version )
# The extra `-` split is to handle pre-release and source built
# versions like 4.5.0-alpha0.435.master.gcb114252b
self . _powerdns_version = [
int ( p . split ( ' - ' ) [ 0 ] ) for p in version . split ( ' . ' ) [ : 3 ] ]
return self . _powerdns_version
@property
def soa_edit_api ( self ) :
# >>> [4, 4, 3] >= [4, 3]
# True
# >>> [4, 3, 3] >= [4, 3]
# True
# >>> [4, 1, 3] >= [4, 3]
# False
if self . powerdns_version > = [ 4 , 3 ] :
return ' DEFAULT '
return ' INCEPTION-INCREMENT '
@property
def check_status_not_found ( self ) :
# >=4.2.x returns 404 when not found
return self . powerdns_version > = [ 4 , 2 ]
def populate ( self , zone , target = False , lenient = False ) :
self . log . debug ( ' populate: name= %s , target= %s , lenient= %s ' , zone . name ,
target , lenient )
resp = None
try :
resp = self . _get ( f ' zones/{zone.name} ' )
self . log . debug ( ' populate: loaded ' )
except HTTPError as e :
error = self . _get_error ( e )
if e . response . status_code == 401 :
# Nicer error message for auth problems
raise Exception ( f ' PowerDNS unauthorized host={self.host} ' )
elif e . response . status_code == 404 \
and self . check_status_not_found :
# 404 means powerdns doesn't know anything about the requested
# domain. We'll just ignore it here and leave the zone
# untouched.
pass
elif e . response . status_code == 422 \
and error . startswith ( ' Could not find domain ' ) \
and not self . check_status_not_found :
# 422 means powerdns doesn't know anything about the requested
# domain. We'll just ignore it here and leave the zone
# untouched.
pass
else :
# just re-throw
raise
before = len ( zone . records )
exists = False
if resp :
exists = True
for rrset in resp . json ( ) [ ' rrsets ' ] :
_type = rrset [ ' type ' ]
if _type == ' SOA ' :
continue
data_for = getattr ( self , f ' _data_for_{_type} ' )
record_name = zone . hostname_from_fqdn ( rrset [ ' name ' ] )
record = Record . new ( zone , record_name , data_for ( rrset ) ,
source = self , lenient = lenient )
zone . add_record ( record , lenient = lenient )
self . log . info ( ' populate: found %s records, exists= %s ' ,
len ( zone . records ) - before , exists )
return exists
def _records_for_multiple ( self , record ) :
return [ { ' content ' : v , ' disabled ' : False }
for v in record . values ]
_records_for_A = _records_for_multiple
_records_for_AAAA = _records_for_multiple
_records_for_NS = _records_for_multiple
def _records_for_CAA ( self , record ) :
return [ {
' content ' : f ' {v.flags} {v.tag} " {v.value} " ' ,
' disabled ' : False
} for v in record . values ]
def _records_for_single ( self , record ) :
return [ { ' content ' : record . value , ' disabled ' : False } ]
_records_for_ALIAS = _records_for_single
_records_for_CNAME = _records_for_single
_records_for_PTR = _records_for_single
def _records_for_quoted ( self , record ) :
return [ { ' content ' : f ' " {v} " ' , ' disabled ' : False }
for v in record . values ]
_records_for_SPF = _records_for_quoted
_records_for_TXT = _records_for_quoted
def _records_for_LOC ( self , record ) :
return [ {
' content ' :
' %d %d %0.3f %s %d %d %.3f %s %0.2f m %0.2f m %0.2f m %0.2f m ' %
(
int ( v . lat_degrees ) ,
int ( v . lat_minutes ) ,
float ( v . lat_seconds ) ,
v . lat_direction ,
int ( v . long_degrees ) ,
int ( v . long_minutes ) ,
float ( v . long_seconds ) ,
v . long_direction ,
float ( v . altitude ) ,
float ( v . size ) ,
float ( v . precision_horz ) ,
float ( v . precision_vert )
) ,
' disabled ' : False
} for v in record . values ]
def _records_for_MX ( self , record ) :
return [ {
' content ' : f ' {v.preference} {v.exchange} ' ,
' disabled ' : False
} for v in record . values ]
def _records_for_NAPTR ( self , record ) :
return [ {
' content ' : f ' {v.order} {v.preference} " {v.flags} " " {v.service} " '
f ' " {v.regexp} " {v.replacement} ' ,
' disabled ' : False
} for v in record . values ]
def _records_for_SSHFP ( self , record ) :
return [ {
' content ' : f ' {v.algorithm} {v.fingerprint_type} {v.fingerprint} ' ,
' disabled ' : False
} for v in record . values ]
def _records_for_SRV ( self , record ) :
return [ {
' content ' : f ' {v.priority} {v.weight} {v.port} {v.target} ' ,
' disabled ' : False
} for v in record . values ]
def _mod_Create ( self , change ) :
new = change . new
records_for = getattr ( self , f ' _records_for_{new._type} ' )
return {
' name ' : new . fqdn ,
' type ' : new . _type ,
' ttl ' : new . ttl ,
' changetype ' : ' REPLACE ' ,
' records ' : records_for ( new )
}
_mod_Update = _mod_Create
def _mod_Delete ( self , change ) :
existing = change . existing
records_for = getattr ( self , f ' _records_for_{existing._type} ' )
return {
' name ' : existing . fqdn ,
' type ' : existing . _type ,
' ttl ' : existing . ttl ,
' changetype ' : ' DELETE ' ,
' records ' : records_for ( existing )
}
def _get_nameserver_record ( self , existing ) :
return None
def _extra_changes ( self , existing , * * kwargs ) :
self . log . debug ( ' _extra_changes: zone= %s ' , existing . name )
ns = self . _get_nameserver_record ( existing )
if not ns :
return [ ]
# sorting mostly to make things deterministic for testing, but in
# theory it let us find what we're after quicker (though sorting would
# be more expensive.)
for record in sorted ( existing . records ) :
if record == ns :
# We've found the top-level NS record, return any changes
change = record . changes ( ns , self )
self . log . debug ( ' _extra_changes: change= %s ' , change )
if change :
# We need to modify an existing record
return [ change ]
# No change is necessary
return [ ]
# No existing top-level NS
self . log . debug ( ' _extra_changes: create ' )
return [ Create ( ns ) ]
def _get_error ( self , http_error ) :
try :
return http_error . response . json ( ) [ ' error ' ]
except Exception :
return ' '
def _apply ( self , plan ) :
desired = plan . desired
changes = plan . changes
self . log . debug ( ' _apply: zone= %s , len(changes)= %d ' , desired . name ,
len ( changes ) )
mods = [ ]
for change in changes :
class_name = change . __class__ . __name__
mods . append ( getattr ( self , f ' _mod_{class_name} ' ) ( change ) )
# Ensure that any DELETE modifications always occur before any REPLACE
# modifications. This ensures that an A record can be replaced by a
# CNAME record and vice-versa.
mods . sort ( key = itemgetter ( ' changetype ' ) )
self . log . debug ( ' _apply: sending change request ' )
try :
self . _patch ( f ' zones/{desired.name} ' , data = { ' rrsets ' : mods } )
self . log . debug ( ' _apply: patched ' )
except HTTPError as e :
error = self . _get_error ( e )
if not (
(
e . response . status_code == 404 and
self . check_status_not_found
) or (
e . response . status_code == 422 and
error . startswith ( ' Could not find domain ' ) and
not self . check_status_not_found
)
) :
self . log . error (
' _apply: status= %d , text= %s ' ,
e . response . status_code ,
e . response . text )
raise
self . log . info ( ' _apply: creating zone= %s ' , desired . name )
# 404 or 422 means powerdns doesn't know anything about the
# requested domain. We'll try to create it with the correct
# records instead of update. Hopefully all the mods are
# creates :-)
data = {
' name ' : desired . name ,
' kind ' : ' Master ' ,
' masters ' : [ ] ,
' nameservers ' : [ ] ,
' rrsets ' : mods ,
' soa_edit_api ' : self . soa_edit_api ,
' serial ' : 0 ,
}
try :
self . _post ( ' zones ' , data )
except HTTPError as e :
self . log . error ( ' _apply: status= %d , text= %s ' ,
e . response . status_code ,
e . response . text )
raise
self . log . debug ( ' _apply: created ' )
self . log . debug ( ' _apply: complete ' )
class PowerDnsProvider ( PowerDnsBaseProvider ) :
'''
PowerDNS API v4 Provider
powerdns :
class : octodns . provider . powerdns . PowerDnsProvider
# The host on which PowerDNS api is listening (required)
host : fqdn
# The api key that grans access (required)
api_key : api - key
# The port on which PowerDNS api is listening (optional, default 8081)
port : 8081
# The nameservers to use for this provider (optional,
# default unmanaged)
nameserver_values :
- 1.2 . 3.4 .
- 1.2 . 3.5 .
# The nameserver record TTL when managed, (optional, default 600)
nameserver_ttl : 600
'''
def __init__ ( self , id , host , api_key , port = 8081 , nameserver_values = None ,
nameserver_ttl = 600 ,
* args , * * kwargs ) :
self . log = logging . getLogger ( f ' PowerDnsProvider[{id}] ' )
self . log . debug ( ' __init__: id= %s , host= %s , port= %d , '
' nameserver_values= %s , nameserver_ttl= %d ' ,
id , host , port , nameserver_values , nameserver_ttl )
super ( PowerDnsProvider , self ) . __init__ ( id , host = host , api_key = api_key ,
port = port ,
* args , * * kwargs )
self . nameserver_values = nameserver_values
self . nameserver_ttl = nameserver_ttl
def _get_nameserver_record ( self , existing ) :
if self . nameserver_values :
return Record . new ( existing , ' ' , {
' type ' : ' NS ' ,
' ttl ' : self . nameserver_ttl ,
' values ' : self . nameserver_values ,
} , source = self )
return super ( PowerDnsProvider , self ) . _get_nameserver_record ( existing )
from logging import getLogger
logger = getLogger ( ' PowerDns ' )
try :
logger . warn ( ' octodns_powerdns shimmed. Update your provider class to '
' octodns_powerdns.PowerDnsProvider. '
' Shim will be removed in 1.0 ' )
from octodns_powerdns import PowerDnsProvider , PowerDnsBaseProvider
PowerDnsProvider # pragma: no cover
PowerDnsBaseProvider # pragma: no cover
except ModuleNotFoundError :
logger . exception ( ' PowerDnsProvider has been moved into a seperate module, '
' octodns_powerdns is now required. Provider class should '
' be updated to octodns_powerdns.PowerDnsProvider ' )
raise