9552ccc2c9
Change-Id: I18d9a21b75053d382fb5bbebba226eda4c174f0c
366 lines
13 KiB
Python
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 impl_ipa.rectype2iparectype.iteritems():
|
|
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.warn(_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 dservers.iteritems():
|
|
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.itervalues():
|
|
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 DuplicateDomain 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 zrec.iteritems():
|
|
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 zonerecs.iteritems():
|
|
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()
|