Merge "Removing old archive code"
This commit is contained in:
commit
05e30f87f1
@ -1,437 +0,0 @@
|
|||||||
# Copyright 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 pprint
|
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_serialization import jsonutils as json
|
|
||||||
from oslo_utils import importutils
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from designate.backend import base
|
|
||||||
from designate import exceptions
|
|
||||||
from designate.i18n import _LE
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
IPA_DEFAULT_PORT = 443
|
|
||||||
|
|
||||||
|
|
||||||
class IPABaseError(exceptions.Backend):
|
|
||||||
error_code = 500
|
|
||||||
error_type = 'unknown_ipa_error'
|
|
||||||
|
|
||||||
|
|
||||||
class IPAAuthError(IPABaseError):
|
|
||||||
error_type = 'authentication_error'
|
|
||||||
|
|
||||||
|
|
||||||
# map of designate domain parameters to the corresponding
|
|
||||||
# ipa parameter
|
|
||||||
# NOTE: ipa manages serial, and does not honor
|
|
||||||
# increment_serial=False - this means the designate serial
|
|
||||||
# and the ipa serial will diverge if updates are made
|
|
||||||
# using increment_serial=False
|
|
||||||
domain2ipa = {'ttl': 'dnsttl', 'email': 'idnssoarname',
|
|
||||||
'serial': 'idnssoaserial', 'expire': 'idnssoaexpire',
|
|
||||||
'minimum': 'idnssoaminimum', 'refresh': 'idnssoarefresh',
|
|
||||||
'retry': 'idnssoaretry'}
|
|
||||||
|
|
||||||
# map of designate record types to ipa
|
|
||||||
rectype2iparectype = {'A': ('arecord', '%(data)s'),
|
|
||||||
'AAAA': ('aaaarecord', '%(data)s'),
|
|
||||||
'MX': ('mxrecord', '%(data)s'),
|
|
||||||
'CNAME': ('cnamerecord', '%(data)s'),
|
|
||||||
'TXT': ('txtrecord', '%(data)s'),
|
|
||||||
'SRV': ('srvrecord', '%(data)s'),
|
|
||||||
'NS': ('nsrecord', '%(data)s'),
|
|
||||||
'PTR': ('ptrrecord', '%(data)s'),
|
|
||||||
'SPF': ('spfrecord', '%(data)s'),
|
|
||||||
'SSHFP': ('sshfprecord', '%(data)s'),
|
|
||||||
'NAPTR': ('naptrrecord', '%(data)s'),
|
|
||||||
'CAA': ('caarecord', '%(data)s'),
|
|
||||||
'CERT': ('certrecord', '%(data)s'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IPA_INVALID_DATA = 3009
|
|
||||||
IPA_NOT_FOUND = 4001
|
|
||||||
IPA_DUPLICATE = 4002
|
|
||||||
IPA_NO_CHANGES = 4202
|
|
||||||
|
|
||||||
|
|
||||||
class IPAUnknownError(IPABaseError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPACommunicationFailure(IPABaseError):
|
|
||||||
error_type = 'communication_failure'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPAInvalidData(IPABaseError):
|
|
||||||
error_type = 'invalid_data'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPADomainNotFound(IPABaseError):
|
|
||||||
error_type = 'domain_not_found'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPARecordNotFound(IPABaseError):
|
|
||||||
error_type = 'record_not_found'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPADuplicateDomain(IPABaseError):
|
|
||||||
error_type = 'duplicate_domain'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPADuplicateRecord(IPABaseError):
|
|
||||||
error_type = 'duplicate_record'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
ipaerror2exception = {
|
|
||||||
IPA_INVALID_DATA: {
|
|
||||||
'dnszone': IPAInvalidData,
|
|
||||||
'dnsrecord': IPAInvalidData
|
|
||||||
},
|
|
||||||
IPA_NOT_FOUND: {
|
|
||||||
'dnszone': IPADomainNotFound,
|
|
||||||
'dnsrecord': IPARecordNotFound
|
|
||||||
},
|
|
||||||
IPA_DUPLICATE: {
|
|
||||||
'dnszone': IPADuplicateDomain,
|
|
||||||
'dnsrecord': IPADuplicateRecord
|
|
||||||
},
|
|
||||||
# NOTE: Designate will send updates with all fields
|
|
||||||
# even if they have not changed value. If none of
|
|
||||||
# the given values has changed, IPA will return
|
|
||||||
# this error code - this can be ignored
|
|
||||||
IPA_NO_CHANGES: {
|
|
||||||
'dnszone': None,
|
|
||||||
'dnsrecord': None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def abs2rel_name(domain, rsetname):
|
|
||||||
"""convert rsetname from absolute form foo.bar.tld. to the name
|
|
||||||
relative to the domain. For IPA, if domain is rsetname, then use
|
|
||||||
"@" as the relative name. If rsetname does not end with a subset
|
|
||||||
of the domain, the just return the raw rsetname
|
|
||||||
"""
|
|
||||||
if rsetname.endswith(domain):
|
|
||||||
idx = rsetname.rfind(domain)
|
|
||||||
if idx == 0:
|
|
||||||
rsetname = "@"
|
|
||||||
elif idx > 0:
|
|
||||||
rsetname = rsetname[:idx].rstrip(".")
|
|
||||||
return rsetname
|
|
||||||
|
|
||||||
|
|
||||||
class IPABackend(base.Backend):
|
|
||||||
__plugin_name__ = 'ipa'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_cfg_opts(cls):
|
|
||||||
group = cfg.OptGroup(
|
|
||||||
name='backend:ipa', title="Configuration for IPA Backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
cfg.StrOpt('ipa-host', default='localhost.localdomain',
|
|
||||||
help='IPA RPC listener host - must be FQDN'),
|
|
||||||
cfg.IntOpt('ipa-port', default=IPA_DEFAULT_PORT,
|
|
||||||
help='IPA RPC listener port'),
|
|
||||||
cfg.StrOpt('ipa-client-keytab',
|
|
||||||
help='Kerberos client keytab file'),
|
|
||||||
cfg.StrOpt('ipa-auth-driver-class',
|
|
||||||
default='designate.backend.impl_ipa.auth.IPAAuth',
|
|
||||||
help='Class that implements the authentication '
|
|
||||||
'driver for IPA'),
|
|
||||||
cfg.StrOpt('ipa-ca-cert',
|
|
||||||
help='CA certificate for use with https to IPA'),
|
|
||||||
cfg.StrOpt('ipa-base-url', default='/ipa',
|
|
||||||
help='Base URL for IPA RPC, relative to host[:port]'),
|
|
||||||
cfg.StrOpt('ipa-json-url',
|
|
||||||
default='/json',
|
|
||||||
help='URL for IPA JSON RPC, relative to IPA base URL'),
|
|
||||||
cfg.IntOpt('ipa-connect-retries', default=1,
|
|
||||||
help='How many times Designate will attempt to retry '
|
|
||||||
'the connection to IPA before giving up'),
|
|
||||||
cfg.BoolOpt('ipa-force-ns-use', default=False,
|
|
||||||
help='IPA requires that a specified '
|
|
||||||
'name server or SOA MNAME is resolvable - if this '
|
|
||||||
'option is set, Designate will force IPA to use a '
|
|
||||||
'given name server even if it is not resolvable'),
|
|
||||||
cfg.StrOpt('ipa-version', default='2.65',
|
|
||||||
help='IPA RPC JSON version')
|
|
||||||
]
|
|
||||||
|
|
||||||
return [(group, opts)]
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
LOG.debug('IPABackend start')
|
|
||||||
self.request = requests.Session()
|
|
||||||
authclassname = cfg.CONF[self.name].ipa_auth_driver_class
|
|
||||||
authclass = importutils.import_class(authclassname)
|
|
||||||
self.request.auth = (
|
|
||||||
authclass(cfg.CONF[self.name].ipa_client_keytab,
|
|
||||||
cfg.CONF[self.name].ipa_host))
|
|
||||||
ipa_base_url = cfg.CONF[self.name].ipa_base_url
|
|
||||||
if ipa_base_url.startswith("http"): # full URL
|
|
||||||
self.baseurl = ipa_base_url
|
|
||||||
else: # assume relative to https://host[:port]
|
|
||||||
self.baseurl = "https://" + cfg.CONF[self.name].ipa_host
|
|
||||||
ipa_port = cfg.CONF[self.name].ipa_port
|
|
||||||
if ipa_port != IPA_DEFAULT_PORT:
|
|
||||||
self.baseurl += ":" + str(ipa_port)
|
|
||||||
self.baseurl += ipa_base_url
|
|
||||||
ipa_json_url = cfg.CONF[self.name].ipa_json_url
|
|
||||||
if ipa_json_url.startswith("http"): # full URL
|
|
||||||
self.jsonurl = ipa_json_url
|
|
||||||
else: # assume relative to https://host[:port]
|
|
||||||
self.jsonurl = self.baseurl + ipa_json_url
|
|
||||||
xtra_hdrs = {'Content-Type': 'application/json',
|
|
||||||
'Referer': self.baseurl}
|
|
||||||
self.request.headers.update(xtra_hdrs)
|
|
||||||
self.request.verify = cfg.CONF[self.name].ipa_ca_cert
|
|
||||||
self.ntries = cfg.CONF[self.name].ipa_connect_retries
|
|
||||||
self.force = cfg.CONF[self.name].ipa_force_ns_use
|
|
||||||
|
|
||||||
def create_zone(self, context, zone):
|
|
||||||
LOG.debug('Create Zone %r' % zone)
|
|
||||||
ipareq = {'method': 'dnszone_add', 'id': 0}
|
|
||||||
params = [zone['name']]
|
|
||||||
servers = self.central_service.get_zone_ns_records(self.admin_context)
|
|
||||||
# just use the first one for zone creation - add the others
|
|
||||||
# later, below - use force because designate assumes the NS
|
|
||||||
# already exists somewhere, is resolvable, and already has
|
|
||||||
# an A/AAAA record
|
|
||||||
args = {'idnssoamname': servers[0]['name']}
|
|
||||||
if self.force:
|
|
||||||
args['force'] = True
|
|
||||||
for dkey, ipakey in list(domain2ipa.items()):
|
|
||||||
if dkey in zone:
|
|
||||||
args[ipakey] = zone[dkey]
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
# add NS records for all of the other servers
|
|
||||||
if len(servers) > 1:
|
|
||||||
ipareq = {'method': 'dnsrecord_add', 'id': 0}
|
|
||||||
params = [zone['name'], "@"]
|
|
||||||
args = {'nsrecord': servers[1:]}
|
|
||||||
if self.force:
|
|
||||||
args['force'] = True
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def update_zone(self, context, zone):
|
|
||||||
LOG.debug('Update Zone %r' % zone)
|
|
||||||
ipareq = {'method': 'dnszone_mod', 'id': 0}
|
|
||||||
params = [zone['name']]
|
|
||||||
args = {}
|
|
||||||
for dkey, ipakey in list(domain2ipa.items()):
|
|
||||||
if dkey in zone:
|
|
||||||
args[ipakey] = zone[dkey]
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def delete_zone(self, context, zone):
|
|
||||||
LOG.debug('Delete Zone %r' % zone)
|
|
||||||
ipareq = {'method': 'dnszone_del', 'id': 0}
|
|
||||||
params = [zone['name']]
|
|
||||||
args = {}
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def create_recordset(self, context, domain, recordset):
|
|
||||||
LOG.debug('Discarding create_recordset call, not-applicable')
|
|
||||||
|
|
||||||
def update_recordset(self, context, domain, recordset):
|
|
||||||
LOG.debug('Update RecordSet %r / %r' % (domain, recordset))
|
|
||||||
# designate allows to update a recordset if there are no
|
|
||||||
# records in it - we should ignore this case
|
|
||||||
if not self._recset_has_records(context, recordset):
|
|
||||||
LOG.debug('No records in %r / %r - skipping' % (domain, recordset))
|
|
||||||
return
|
|
||||||
# The only thing IPA allows is to change the ttl, since that is
|
|
||||||
# stored "per recordset"
|
|
||||||
if 'ttl' not in recordset:
|
|
||||||
return
|
|
||||||
ipareq = {'method': 'dnsrecord_mod', 'id': 0}
|
|
||||||
dname = domain['name']
|
|
||||||
rsetname = abs2rel_name(dname, recordset['name'])
|
|
||||||
params = [domain['name'], rsetname]
|
|
||||||
args = {'dnsttl': recordset['ttl']}
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def delete_recordset(self, context, domain, recordset):
|
|
||||||
LOG.debug('Delete RecordSet %r / %r' % (domain, recordset))
|
|
||||||
# designate allows to delete a recordset if there are no
|
|
||||||
# records in it - we should ignore this case
|
|
||||||
if not self._recset_has_records(context, recordset):
|
|
||||||
LOG.debug('No records in %r / %r - skipping' % (domain, recordset))
|
|
||||||
return
|
|
||||||
ipareq = {'method': 'dnsrecord_mod', 'id': 0}
|
|
||||||
dname = domain['name']
|
|
||||||
rsetname = abs2rel_name(dname, recordset['name'])
|
|
||||||
params = [domain['name'], rsetname]
|
|
||||||
rsettype = rectype2iparectype[recordset['type']][0]
|
|
||||||
args = {rsettype: None}
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def create_record(self, context, domain, recordset, record):
|
|
||||||
LOG.debug('Create Record %r / %r / %r' % (domain, recordset, record))
|
|
||||||
ipareq = {'method': 'dnsrecord_add', 'id': 0}
|
|
||||||
params, args = self._rec_to_ipa_rec(domain, recordset, [record])
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def update_record(self, context, domain, recordset, record):
|
|
||||||
LOG.debug('Update Record %r / %r / %r' % (domain, recordset, record))
|
|
||||||
# for modify operations - IPA does not support a way to change
|
|
||||||
# a particular field in a given record - e.g. for an MX record
|
|
||||||
# with several values, IPA stores them like this:
|
|
||||||
# name: "server1.local."
|
|
||||||
# data: ["10 mx1.server1.local.", "20 mx2.server1.local."]
|
|
||||||
# we could do a search of IPA, compare the values in the
|
|
||||||
# returned array - but that adds an additional round trip
|
|
||||||
# and is error prone
|
|
||||||
# instead, we just get all of the current values and send
|
|
||||||
# them in one big modify
|
|
||||||
criteria = {'recordset_id': record['recordset_id']}
|
|
||||||
reclist = self.central_service.find_records(self.admin_context,
|
|
||||||
criteria)
|
|
||||||
ipareq = {'method': 'dnsrecord_mod', 'id': 0}
|
|
||||||
params, args = self._rec_to_ipa_rec(domain, recordset, reclist)
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def delete_record(self, context, domain, recordset, record):
|
|
||||||
LOG.debug('Delete Record %r / %r / %r' % (domain, recordset, record))
|
|
||||||
ipareq = {'method': 'dnsrecord_del', 'id': 0}
|
|
||||||
params, args = self._rec_to_ipa_rec(domain, recordset, [record])
|
|
||||||
args['del_all'] = 0
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
|
|
||||||
def ping(self, context):
|
|
||||||
LOG.debug('Ping')
|
|
||||||
# NOTE: This call will cause ipa to issue an error, but
|
|
||||||
# 1) it should not throw an exception
|
|
||||||
# 2) the response will indicate ipa is running
|
|
||||||
# 3) the bandwidth usage is minimal
|
|
||||||
ipareq = {'method': 'dnszone_show', 'id': 0}
|
|
||||||
params = ['@']
|
|
||||||
args = {}
|
|
||||||
ipareq['params'] = [params, args]
|
|
||||||
retval = {'result': True}
|
|
||||||
try:
|
|
||||||
self._call_and_handle_error(ipareq)
|
|
||||||
except Exception as e:
|
|
||||||
retval = {'result': False, 'reason': str(e)}
|
|
||||||
return retval
|
|
||||||
|
|
||||||
def _rec_to_ipa_rec(self, domain, recordset, reclist):
|
|
||||||
dname = domain['name']
|
|
||||||
rsetname = abs2rel_name(dname, recordset['name'])
|
|
||||||
params = [dname, rsetname]
|
|
||||||
rectype = recordset['type']
|
|
||||||
vals = []
|
|
||||||
for record in reclist:
|
|
||||||
vals.append(rectype2iparectype[rectype][1] % record)
|
|
||||||
args = {rectype2iparectype[rectype][0]: vals}
|
|
||||||
ttl = recordset.get('ttl') or domain.get('ttl')
|
|
||||||
if ttl is not None:
|
|
||||||
args['dnsttl'] = ttl
|
|
||||||
return params, args
|
|
||||||
|
|
||||||
def _ipa_error_to_exception(self, resp, ipareq):
|
|
||||||
exc = None
|
|
||||||
if resp['error'] is None:
|
|
||||||
return exc
|
|
||||||
errcode = resp['error']['code']
|
|
||||||
method = ipareq['method']
|
|
||||||
methtype = method.split('_')[0]
|
|
||||||
exclass = ipaerror2exception.get(errcode, {}).get(methtype,
|
|
||||||
IPAUnknownError)
|
|
||||||
if exclass:
|
|
||||||
LOG.debug("Error: ipa command [%s] returned error [%s]" %
|
|
||||||
(pprint.pformat(ipareq), pprint.pformat(resp)))
|
|
||||||
elif errcode: # not mapped
|
|
||||||
LOG.debug("Ignoring IPA error code %d: %s" %
|
|
||||||
(errcode, pprint.pformat(resp)))
|
|
||||||
return exclass
|
|
||||||
|
|
||||||
def _call_and_handle_error(self, ipareq):
|
|
||||||
if 'version' not in ipareq['params'][1]:
|
|
||||||
ipareq['params'][1]['version'] = cfg.CONF[self.name].ipa_version
|
|
||||||
need_reauth = False
|
|
||||||
while True:
|
|
||||||
status_code = 200
|
|
||||||
try:
|
|
||||||
if need_reauth:
|
|
||||||
self.request.auth.refresh_auth()
|
|
||||||
rawresp = self.request.post(self.jsonurl,
|
|
||||||
data=json.dumps(ipareq))
|
|
||||||
status_code = rawresp.status_code
|
|
||||||
except IPAAuthError:
|
|
||||||
status_code = 401
|
|
||||||
if status_code == 401:
|
|
||||||
if self.ntries == 0:
|
|
||||||
# persistent inability to auth
|
|
||||||
LOG.error(_LE("Error: could not authenticate to IPA - "
|
|
||||||
"please check for correct keytab file"))
|
|
||||||
# reset for next time
|
|
||||||
self.ntries = cfg.CONF[self.name].ipa_connect_retries
|
|
||||||
raise IPACommunicationFailure()
|
|
||||||
else:
|
|
||||||
LOG.debug("Refresh authentication")
|
|
||||||
need_reauth = True
|
|
||||||
self.ntries -= 1
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
# successful - reset
|
|
||||||
self.ntries = cfg.CONF[self.name].ipa_connect_retries
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
resp = json.loads(rawresp.text)
|
|
||||||
except ValueError:
|
|
||||||
# response was not json - some sort of error response
|
|
||||||
LOG.debug("Error: unknown error from IPA [%s]" % rawresp.text)
|
|
||||||
raise IPAUnknownError("unable to process response from IPA")
|
|
||||||
# raise the appropriate exception, if error
|
|
||||||
exclass = self._ipa_error_to_exception(resp, ipareq)
|
|
||||||
if exclass:
|
|
||||||
# could add additional info/message to exception here
|
|
||||||
raise exclass()
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def _recset_has_records(self, context, recordset):
|
|
||||||
"""Return True if the recordset has records, False otherwise"""
|
|
||||||
criteria = {'recordset_id': recordset['id']}
|
|
||||||
num = self.central_service.count_records(self.admin_context,
|
|
||||||
criteria)
|
|
||||||
return num > 0
|
|
@ -1,62 +0,0 @@
|
|||||||
# Copyright 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 logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
import kerberos
|
|
||||||
from requests import auth
|
|
||||||
|
|
||||||
from designate.backend.impl_ipa import IPAAuthError
|
|
||||||
from designate.i18n import _LE
|
|
||||||
from designate.i18n import _LW
|
|
||||||
from designate.utils import generate_uuid
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAAuth(auth.AuthBase):
|
|
||||||
def __init__(self, keytab, hostname):
|
|
||||||
# store the kerberos credentials in memory rather than on disk
|
|
||||||
os.environ['KRB5CCNAME'] = "MEMORY:" + generate_uuid()
|
|
||||||
self.token = None
|
|
||||||
self.keytab = keytab
|
|
||||||
self.hostname = hostname
|
|
||||||
if self.keytab:
|
|
||||||
os.environ['KRB5_CLIENT_KTNAME'] = self.keytab
|
|
||||||
else:
|
|
||||||
LOG.warning(_LW('No IPA client kerberos keytab file given'))
|
|
||||||
|
|
||||||
def __call__(self, request):
|
|
||||||
if not self.token:
|
|
||||||
self.refresh_auth()
|
|
||||||
request.headers['Authorization'] = 'negotiate ' + self.token
|
|
||||||
return request
|
|
||||||
|
|
||||||
def refresh_auth(self):
|
|
||||||
service = "HTTP@" + self.hostname
|
|
||||||
flags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG
|
|
||||||
try:
|
|
||||||
(_, vc) = kerberos.authGSSClientInit(service, flags)
|
|
||||||
except kerberos.GSSError as e:
|
|
||||||
LOG.error(_LE("caught kerberos exception %r") % e)
|
|
||||||
raise IPAAuthError(str(e))
|
|
||||||
try:
|
|
||||||
kerberos.authGSSClientStep(vc, "")
|
|
||||||
except kerberos.GSSError as e:
|
|
||||||
LOG.error(_LE("caught kerberos exception %r") % e)
|
|
||||||
raise IPAAuthError(str(e))
|
|
||||||
self.token = kerberos.authGSSClientResponse(vc)
|
|
@ -1,153 +0,0 @@
|
|||||||
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
|
|
||||||
#
|
|
||||||
# Author: Artom Lifshitz <artom.lifshitz@enovance.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 logging
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_utils import excutils
|
|
||||||
|
|
||||||
from designate import backend
|
|
||||||
from designate.backend import base
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
CFG_GROUP = 'backend:multi'
|
|
||||||
|
|
||||||
|
|
||||||
class MultiBackend(base.Backend):
|
|
||||||
"""
|
|
||||||
Multi-backend backend
|
|
||||||
|
|
||||||
This backend dispatches calls to a master backend and a slave backend.
|
|
||||||
It enforces master/slave ordering semantics as follows:
|
|
||||||
|
|
||||||
Creates for tsigkeys, servers and domains are done on the master first,
|
|
||||||
then on the slave.
|
|
||||||
|
|
||||||
Updates for tsigkeys, servers and domains and all operations on records
|
|
||||||
are done on the master only. It's assumed masters and slaves use an
|
|
||||||
external mechanism to sync existing domains, most likely XFR.
|
|
||||||
|
|
||||||
Deletes are done on the slave first, then on the master.
|
|
||||||
|
|
||||||
If the create on the slave fails, the domain/tsigkey/server is deleted from
|
|
||||||
the master. If delete on the master fails, the domain/tdigkey/server is
|
|
||||||
recreated on the slave.
|
|
||||||
"""
|
|
||||||
__plugin_name__ = 'multi'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_cfg_opts(cls):
|
|
||||||
group = cfg.OptGroup(
|
|
||||||
name=CFG_GROUP, title="Configuration for multi-backend Backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
cfg.StrOpt('master', default='fake', help='Master backend'),
|
|
||||||
cfg.StrOpt('slave', default='fake', help='Slave backend'),
|
|
||||||
]
|
|
||||||
|
|
||||||
return [(group, opts)]
|
|
||||||
|
|
||||||
def __init__(self, central_service):
|
|
||||||
super(MultiBackend, self).__init__(central_service)
|
|
||||||
self.central = central_service
|
|
||||||
self.master = backend.get_backend(cfg.CONF[CFG_GROUP].master,
|
|
||||||
central_service)
|
|
||||||
self.slave = backend.get_backend(cfg.CONF[CFG_GROUP].slave,
|
|
||||||
central_service)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.master.start()
|
|
||||||
self.slave.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.slave.stop()
|
|
||||||
self.master.stop()
|
|
||||||
|
|
||||||
def create_tsigkey(self, context, tsigkey):
|
|
||||||
self.master.create_tsigkey(context, tsigkey)
|
|
||||||
try:
|
|
||||||
self.slave.create_tsigkey(context, tsigkey)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.master.delete_tsigkey(context, tsigkey)
|
|
||||||
|
|
||||||
def update_tsigkey(self, context, tsigkey):
|
|
||||||
self.master.update_tsigkey(context, tsigkey)
|
|
||||||
|
|
||||||
def delete_tsigkey(self, context, tsigkey):
|
|
||||||
self.slave.delete_tsigkey(context, tsigkey)
|
|
||||||
try:
|
|
||||||
self.master.delete_tsigkey(context, tsigkey)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.slave.create_tsigkey(context, tsigkey)
|
|
||||||
|
|
||||||
def create_zone(self, context, zone):
|
|
||||||
self.master.create_zone(context, zone)
|
|
||||||
try:
|
|
||||||
self.slave.create_zone(context, zone)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.master.delete_zone(context, zone)
|
|
||||||
|
|
||||||
def update_zone(self, context, zone):
|
|
||||||
self.master.update_zone(context, zone)
|
|
||||||
|
|
||||||
def delete_zone(self, context, zone):
|
|
||||||
# Fetch the full zone from Central first, as we may
|
|
||||||
# have to recreate it on slave if delete on master fails
|
|
||||||
deleted_context = context.deepcopy()
|
|
||||||
deleted_context.show_deleted = True
|
|
||||||
|
|
||||||
full_domain = self.central.find_zone(
|
|
||||||
deleted_context, {'id': zone['id']})
|
|
||||||
|
|
||||||
self.slave.delete_zone(context, zone)
|
|
||||||
try:
|
|
||||||
self.master.delete_zone(context, zone)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.slave.create_zone(context, zone)
|
|
||||||
|
|
||||||
[self.slave.create_record(context, zone, record)
|
|
||||||
for record in self.central.find_records(
|
|
||||||
context, {'domain_id': full_domain['id']})]
|
|
||||||
|
|
||||||
def create_recordset(self, context, zone, recordset):
|
|
||||||
self.master.create_recordset(context, zone, recordset)
|
|
||||||
|
|
||||||
def update_recordset(self, context, zone, recordset):
|
|
||||||
self.master.update_recordset(context, zone, recordset)
|
|
||||||
|
|
||||||
def delete_recordset(self, context, zone, recordset):
|
|
||||||
self.master.delete_recordset(context, zone, recordset)
|
|
||||||
|
|
||||||
def create_record(self, context, zone, recordset, record):
|
|
||||||
self.master.create_record(context, zone, recordset, record)
|
|
||||||
|
|
||||||
def update_record(self, context, zone, recordset, record):
|
|
||||||
self.master.update_record(context, zone, recordset, record)
|
|
||||||
|
|
||||||
def delete_record(self, context, zone, recordset, record):
|
|
||||||
self.master.delete_record(context, zone, recordset, record)
|
|
||||||
|
|
||||||
def ping(self, context):
|
|
||||||
return {
|
|
||||||
'master': self.master.ping(context),
|
|
||||||
'slave': self.slave.ping(context)
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
# Copyright 2016 Rackspace, Inc.
|
|
||||||
#
|
|
||||||
# Author: Tim Simmons <tim.simmons@rackspace.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.
|
|
||||||
"""
|
|
||||||
This dumb script allows you to see what's being dumped onto
|
|
||||||
the notifications.info queue
|
|
||||||
|
|
||||||
nabbed from:
|
|
||||||
https://pika.readthedocs.io/en/latest/examples/blocking_consume.html
|
|
||||||
"""
|
|
||||||
import pika
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(channel, method_frame, header_frame, body):
|
|
||||||
print(method_frame.delivery_tag)
|
|
||||||
print(body)
|
|
||||||
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
|
|
||||||
|
|
||||||
|
|
||||||
connection = pika.BlockingConnection()
|
|
||||||
channel = connection.channel()
|
|
||||||
channel.basic_consume(on_message, 'notifications.info')
|
|
||||||
try:
|
|
||||||
channel.start_consuming()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
channel.stop_consuming()
|
|
||||||
connection.close()
|
|
@ -1,2 +0,0 @@
|
|||||||
requests
|
|
||||||
kerberos
|
|
Loading…
x
Reference in New Issue
Block a user