designate/contrib/ipaextractor.py

366 lines
13 KiB
Python

# Copyright (C) 2014 Red Hat, Inc.
#
# Author: Rich Megginson <rmeggins@redhat.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
import logging
import uuid
import pprint
import json
import copy
import requests
from oslo_config import cfg
from designate.backend import impl_ipa
from designate.i18n import _LI
from designate.i18n import _LW
from designate.i18n import _LE
from designate import utils
logging.basicConfig()
LOG = logging.getLogger(__name__)
cfg.CONF.import_opt('api_base_uri', 'designate.api', 'service:api')
cfg.CONF.import_opt('backend_driver', 'designate.central', 'service:central')
class NoNameServers(Exception):
pass
class AddServerError(Exception):
pass
class DeleteServerError(Exception):
pass
class AddDomainError(Exception):
pass
class DeleteDomainError(Exception):
pass
class AddRecordError(Exception):
pass
cuiberrorstr = """ERROR: You cannot have Designate configured
to use the IPA backend when running this script. It will wipe
out your IPA DNS data. Please follow these steps:
* shutdown designate-central
* edit designate.conf
[service:central]
backend_driver = fake # or something other than ipa
* restart designate-central and other designate services
"""
class CannotUseIPABackend(Exception):
pass
# create mapping of ipa record types to designate types
iparectype2designate = {}
for rectype, tup in list(impl_ipa.rectype2iparectype.items()):
iparectype = tup[0]
iparectype2designate[iparectype] = rectype
# using the all: True flag returns fields we can't use
# strip these keys from zones
zoneskips = ['dn', 'nsrecord', 'idnszoneactive', 'objectclass']
def rec2des(rec, zonename):
"""Convert an IPA record to Designate format. A single IPA record
returned from the search may translate into multiple Designate.
IPA dnsrecord_find returns a "name". Each DNS name may contain
multiple record types. Each record type may contain multiple
values. Each one of these values must be added separately to
Designate. This function returns all of those as a list of
dict designate records.
"""
# convert record name
if rec['idnsname'][0] == '@':
name = zonename
else:
name = rec['idnsname'][0] + "." + zonename
# find all record types
rectypes = []
for k in rec:
if k.endswith("record"):
if k in iparectype2designate:
rectypes.append(k)
else:
LOG.info(_LI("Skipping unknown record type "
"%(type)s in %(name)s"),
{'type': k, 'name': name})
desrecs = []
for rectype in rectypes:
dtype = iparectype2designate[rectype]
for ddata in rec[rectype]:
desreq = {'name': name, 'type': dtype}
if dtype == 'SRV' or dtype == 'MX':
# split off the priority and send in a separate field
idx = ddata.find(' ')
desreq['priority'] = int(ddata[:idx])
if dtype == 'SRV' and not ddata.endswith("."):
# if server is specified as relative, add zonename
desreq['data'] = ddata[(idx + 1):] + "." + zonename
else:
desreq['data'] = ddata[(idx + 1):]
else:
desreq['data'] = ddata
if rec.get('description', [None])[0]:
desreq['description'] = rec.get('description')[0]
if rec.get('ttl', [None])[0]:
desreq['ttl'] = int(rec['dnsttl'][0])
desrecs.append(desreq)
return desrecs
def zone2des(ipazone):
# next, try to add the fake domain to Designate
zonename = ipazone['idnsname'][0].rstrip(".") + "."
email = ipazone['idnssoarname'][0].rstrip(".").replace(".", "@", 1)
desreq = {"name": zonename,
"ttl": int(ipazone['idnssoarefresh'][0]),
"email": email}
return desreq
def getipadomains(ipabackend, version):
# get the list of domains/zones from IPA
ipareq = {'method': 'dnszone_find',
'params': [[], {'version': version,
'all': True}]}
iparesp = ipabackend._call_and_handle_error(ipareq)
LOG.debug("Response: %s" % pprint.pformat(iparesp))
return iparesp['result']['result']
def getiparecords(ipabackend, zonename, version):
ipareq = {'method': 'dnsrecord_find',
'params': [[zonename], {"version": version,
"all": True}]}
iparesp = ipabackend._call_and_handle_error(ipareq)
return iparesp['result']['result']
def syncipaservers2des(servers, designatereq, designateurl):
# get existing servers from designate
dservers = {}
srvurl = designateurl + "/servers"
resp = designatereq.get(srvurl)
LOG.debug("Response: %s" % pprint.pformat(resp.json()))
if resp and resp.status_code == 200 and resp.json() and \
'servers' in resp.json():
for srec in resp.json()['servers']:
dservers[srec['name']] = srec['id']
else:
LOG.warning(_LW("No servers in designate"))
# first - add servers from ipa not already in designate
for server in servers:
if server in dservers:
LOG.info(_LI("Skipping ipa server %s already in designate"),
server)
else:
desreq = {"name": server}
resp = designatereq.post(srvurl, data=json.dumps(desreq))
LOG.debug("Response: %s" % pprint.pformat(resp.json()))
if resp.status_code == 200:
LOG.info(_LI("Added server %s to designate"), server)
else:
raise AddServerError("Unable to add %s: %s" %
(server, pprint.pformat(resp.json())))
# next - delete servers in designate not in ipa
for server, sid in list(dservers.items()):
if server not in servers:
delresp = designatereq.delete(srvurl + "/" + sid)
if delresp.status_code == 200:
LOG.info(_LI("Deleted server %s"), server)
else:
raise DeleteServerError("Unable to delete %s: %s" %
(server,
pprint.pformat(delresp.json())))
def main():
# HACK HACK HACK - allow required config params to be passed
# via the command line
cfg.CONF['service:api']._group._opts['api_base_uri']['cli'] = True
for optdict in cfg.CONF['backend:ipa']._group._opts.values():
if 'cli' in optdict:
optdict['cli'] = True
# HACK HACK HACK - allow api url to be passed in the usual way
utils.read_config('designate', sys.argv)
if cfg.CONF['service:central'].backend_driver == 'ipa':
raise CannotUseIPABackend(cuiberrorstr)
if cfg.CONF.debug:
LOG.setLevel(logging.DEBUG)
elif cfg.CONF.verbose:
LOG.setLevel(logging.INFO)
else:
LOG.setLevel(logging.WARN)
ipabackend = impl_ipa.IPABackend(None)
ipabackend.start()
version = cfg.CONF['backend:ipa'].ipa_version
designateurl = cfg.CONF['service:api'].api_base_uri + "v1"
# get the list of domains/zones from IPA
ipazones = getipadomains(ipabackend, version)
# get unique list of name servers
servers = {}
for zonerec in ipazones:
for nsrec in zonerec['nsrecord']:
servers[nsrec] = nsrec
if not servers:
raise NoNameServers("Error: no name servers found in IPA")
# let's see if designate is using the IPA backend
# create a fake domain in IPA
# create a fake server in Designate
# try to create the same fake domain in Designate
# if we get a DuplicateZone error from Designate, then
# raise the CannotUseIPABackend error, after deleting
# the fake server and fake domain
# find the first non-reverse zone
zone = {}
for zrec in ipazones:
if not zrec['idnsname'][0].endswith("in-addr.arpa.") and \
zrec['idnszoneactive'][0] == 'TRUE':
# ipa returns every data field as a list
# convert the list to a scalar
for n, v in list(zrec.items()):
if n in zoneskips:
continue
if isinstance(v, list):
zone[n] = v[0]
else:
zone[n] = v
break
assert(zone)
# create a fake subdomain of this zone
domname = "%s.%s" % (uuid.uuid4(), zone['idnsname'])
args = copy.copy(zone)
del args['idnsname']
args['version'] = version
ipareq = {'method': 'dnszone_add',
'params': [[domname], args]}
iparesp = ipabackend._call_and_handle_error(ipareq)
LOG.debug("Response: %s" % pprint.pformat(iparesp))
if iparesp['error']:
raise AddDomainError(pprint.pformat(iparesp))
# set up designate connection
designatereq = requests.Session()
xtra_hdrs = {'Content-Type': 'application/json'}
designatereq.headers.update(xtra_hdrs)
# sync ipa name servers to designate
syncipaservers2des(servers, designatereq, designateurl)
domainurl = designateurl + "/domains"
# next, try to add the fake domain to Designate
email = zone['idnssoarname'].rstrip(".").replace(".", "@", 1)
desreq = {"name": domname,
"ttl": int(zone['idnssoarefresh'][0]),
"email": email}
resp = designatereq.post(domainurl, data=json.dumps(desreq))
exc = None
fakezoneid = None
if resp.status_code == 200:
LOG.info(_LI("Added domain %s"), domname)
fakezoneid = resp.json()['id']
delresp = designatereq.delete(domainurl + "/" + fakezoneid)
if delresp.status_code != 200:
LOG.error(_LE("Unable to delete %(name)s: %(response)s") %
{'name': domname, 'response': pprint.pformat(
delresp.json())})
else:
exc = CannotUseIPABackend(cuiberrorstr)
# cleanup fake stuff
ipareq = {'method': 'dnszone_del',
'params': [[domname], {'version': version}]}
iparesp = ipabackend._call_and_handle_error(ipareq)
LOG.debug("Response: %s" % pprint.pformat(iparesp))
if iparesp['error']:
LOG.error(_LE("%s") % pprint.pformat(iparesp))
if exc:
raise exc
# get and delete existing domains
resp = designatereq.get(domainurl)
LOG.debug("Response: %s" % pprint.pformat(resp.json()))
if resp and resp.status_code == 200 and resp.json() and \
'domains' in resp.json():
# domains must be deleted in child/parent order i.e. delete
# sub-domains before parent domains - simple way to get this
# order is to sort the domains in reverse order of name len
dreclist = sorted(resp.json()['domains'],
key=lambda drec: len(drec['name']),
reverse=True)
for drec in dreclist:
delresp = designatereq.delete(domainurl + "/" + drec['id'])
if delresp.status_code != 200:
raise DeleteDomainError("Unable to delete %s: %s" %
(drec['name'],
pprint.pformat(delresp.json())))
# key is zonename, val is designate rec id
zonerecs = {}
for zonerec in ipazones:
desreq = zone2des(zonerec)
resp = designatereq.post(domainurl, data=json.dumps(desreq))
if resp.status_code == 200:
LOG.info(_LI("Added domain %s"), desreq['name'])
else:
raise AddDomainError("Unable to add domain %s: %s" %
(desreq['name'], pprint.pformat(resp.json())))
zonerecs[desreq['name']] = resp.json()['id']
# get the records for each zone
for zonename, domainid in list(zonerecs.items()):
recurl = designateurl + "/domains/" + domainid + "/records"
iparecs = getiparecords(ipabackend, zonename, version)
for rec in iparecs:
desreqs = rec2des(rec, zonename)
for desreq in desreqs:
resp = designatereq.post(recurl, data=json.dumps(desreq))
if resp.status_code == 200:
LOG.info(_LI("Added record %(record)s "
"for domain %(domain)s"),
{'record': desreq['name'], 'domain': zonename})
else:
raise AddRecordError("Could not add record %s: %s" %
(desreq['name'],
pprint.pformat(resp.json())))
if __name__ == '__main__':
main()