f9c55abbff
The Oslo libraries have moved all of their code out of the 'oslo' namespace package into per-library packages. The namespace package was retained during kilo for backwards compatibility, but will be removed by the liberty-2 milestone. This change removes the use of the namespace package, replacing it with the new package names. The patches in the libraries will be put on hold until application patches have landed, or L2, whichever comes first. At that point, new versions of the libraries without namespace packages will be released as a major version update. Please merge this patch, or an equivalent, before L2 to avoid problems with those library releases. Blueprint: remove-namespace-packages https://blueprints.launchpad.net/oslo-incubator/+spec/remove-namespace-packages Change-Id: Ia21c15e8eca6bf456f7cfe13f815f5ce068601e7
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()
|