add support for FreeIPA backend - phase 4 - migration

There is a script that can be used to initially populate Designate
from FreeIPA called ipaextractor.py.  You use it like this:
1) Change Designate to use backend_driver = fake - this will allow it
to update its internal database but not the ipa backend which we are
importing from
2) run ipaextractor.py with no arguments will use the standard designate.conf
3) run ipaextractor.py --config-file file.conf will use the parameters from the given config file
4) run ipaextractor.py and explicitly pass in the parameters
   ipaextractor.py [--config-file file.conf] \
      --backend:ipa-ipa-host hostname \
      ... other ipa options specified as --backend:ipa-optionname ...
      --service:ipa-api-base-uri=http://designateapihost:port
Where the ipa parameters are as described above
the default designate API URL is https://localhost:9001/
NOTE: if you want to specify both a config file (e.g. for common
options) but you want to override some of those via the command line,
you must specify --config-file file.conf first, before any other specfic
parameters you want to override.
NOTE: ipaextractor cannot be used if designate is using the IPA backend.
ipaextractor will attempt to determine if designate is using the IPA
backend, and will exit with an error if so.

Change-Id: Ic2b9c78e5f5980b62b7e93d000038ac7db921c19
Implements: blueprint ipa-backend
This commit is contained in:
Rich Megginson 2014-04-28 21:33:05 -06:00
parent a6242d12ab
commit cb7050e366
1 changed files with 355 additions and 0 deletions

355
contrib/ipaextractor.py Normal file
View File

@ -0,0 +1,355 @@
# 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 requests
import uuid
import pprint
import json
import copy
from oslo.config import cfg
from designate.backend import impl_ipa
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("Skipping unknown record type %s in %s" %
k, 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("No servers in designate")
# first - add servers from ipa not already in designate
for server in servers:
if server in dservers:
LOG.info("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("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("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("Added domain %s" % domname)
fakezoneid = resp.json()['id']
delresp = designatereq.delete(domainurl + "/" + fakezoneid)
if delresp.status_code != 200:
LOG.error("Unable to delete %s: %s" %
(domname, 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(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("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("Added record %s for domain %s" %
(desreq['name'], zonename))
else:
raise AddRecordError("Could not add record %s: %s" %
(desreq['name'],
pprint.pformat(resp.json())))
if __name__ == '__main__':
main()