def find_zone_serial(self, zone_name): """Get serial from a zone by running knotc :returns: serial (int or None) :raises: exceptions.Backend """ zone_name = zone_name.rstrip('.') LOG.debug("Finding %s", zone_name) # Output example: # [530336536.com.] type: slave | serial: 0 | next-event: idle | # auto-dnssec: disabled] try: out, err = execute(self._knotc_cmd_name, 'zone-status', zone_name) except ProcessExecutionError as e: if 'no such zone' in e.stdout: # Zone not found return None LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend(e) try: serial = out.split('|')[1].split()[1] return int(serial) except Exception as e: LOG.error(_LE("Unable to parse knotc output: %r"), out) raise exceptions.Backend("Unexpected knotc zone-status output")
def _check_dirs(self, *dirnames): """Check if directories are writable """ for dn in dirnames: if not os.path.isdir(dn): raise exceptions.Backend("Missing directory %s" % dn) if not os.access(dn, os.W_OK): raise exceptions.Backend("Directory not writable: %s" % dn)
def _execute_nsd4(self, command): try: LOG.debug('Executing NSD4 control call: %s on %s' % (command, self.host)) result = self._command(command) except (ssl.SSLError, socket.error) as e: LOG.debug('NSD4 control call failure: %s' % e) raise exceptions.Backend(e) if result != 'ok': raise exceptions.Backend(result)
def _rebuild_data_cdb(self): """Rebuild data.cdb file from zone datafiles Requires global lock On zone creation, axfr-get creates datafiles atomically by doing rename. On zone deletion, os.remove deletes the file atomically Globbing and reading the datafiles can be done without locking on them. The data and data.cdb files are written into a unique temp directory """ tmpdir = tempfile.mkdtemp(dir=self._datafiles_dir) data_fn = os.path.join(tmpdir, 'data') tmp_cdb_fn = os.path.join(tmpdir, 'data.cdb') try: self._concatenate_zone_datafiles(data_fn, self._datafiles_path_glob) # Generate the data.cdb file LOG.info("Updating data.cdb") LOG.debug("Convert %s to %s", data_fn, tmp_cdb_fn) try: out, err = execute( cfg.CONF[CFG_GROUP].tinydns_data_cmd_name, cwd=tmpdir ) except ProcessExecutionError as e: LOG.error("Failed to generate data.cdb") LOG.error("Command output: %(out)r Stderr: %(err)r", { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend("Failed to generate data.cdb") LOG.debug("Move %s to %s", tmp_cdb_fn, self._tinydns_cdb_filename) try: os.rename(tmp_cdb_fn, self._tinydns_cdb_filename) except OSError: os.remove(tmp_cdb_fn) LOG.error("Unable to move data.cdb to %s", self._tinydns_cdb_filename) raise exceptions.Backend("Unable to move data.cdb") finally: try: os.remove(data_fn) except OSError: pass try: os.removedirs(tmpdir) except OSError: pass
def _execute_knotc(self, *knotc_args, **kw): """Run the Knot client and check the output :param expected_output: expected output (default: 'OK') :type expected_output: str :param expected_error: expected alternative output, will be \ logged as info(). Default: not set. :type expected_error: str """ # Knotc returns "0" even on failure, we have to check for 'OK' # https://gitlab.labs.nic.cz/labs/knot/issues/456 LOG.debug("Executing knotc with %r", knotc_args) expected = kw.get('expected_output', 'OK') expected_alt = kw.get('expected_error', None) try: out, err = execute(self._knotc_cmd_name, *knotc_args) out = out.rstrip() LOG.debug("Command output: %r" % out) if out != expected: if expected_alt is not None and out == expected_alt: LOG.info(_LI("Ignoring error: %r"), out) else: raise ProcessExecutionError(stdout=out, stderr=err) except ProcessExecutionError as e: LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend(e)
def _check_zone_exists(self, zone): try: requests.get(self._build_url(zone), headers=self.headers).raise_for_status() except requests.HTTPError as e: if e.response.status_code == 404: return False else: LOG.error('HTTP error in check zone exists. Zone %s', zone) raise exceptions.Backend(e) except requests.ConnectionError as e: LOG.error('Connection error in check zone exists. Zone %s', zone) raise exceptions.Backend(e) return True
def _execute_rndc(self, rndc_call): try: LOG.debug('Executing RNDC call: %s' % " ".join(rndc_call)) utils.execute(*rndc_call) except utils.processutils.ProcessExecutionError as e: LOG.debug('RNDC call failure: %s' % e) raise exceptions.Backend(e)
def _perform_axfr_from_minidns(self, zone_name): """Instruct axfr-get to request an AXFR from MiniDNS. :raises: exceptions.Backend on error """ zone_fn = self._datafiles_path_tpl % zone_name zone_tmp_fn = self._datafiles_tmp_path_tpl % zone_name # Perform AXFR, create or update a zone datafile # No need to lock globally here. # Axfr-get creates the datafile atomically by doing rename mdns_hostname, mdns_port = random.choice(self._masters) with lockutils.lock("%s.lock" % zone_name): LOG.debug("writing to %s", zone_fn) cmd = (self._tcpclient_cmd_name, mdns_hostname, "%d" % mdns_port, self._axfr_get_cmd_name, zone_name, zone_fn, zone_tmp_fn) LOG.debug("Executing AXFR as %r", ' '.join(cmd)) try: out, err = execute(*cmd) except ProcessExecutionError as e: LOG.error("Error executing AXFR as %r", ' '.join(cmd)) LOG.error("Command output: %(out)r Stderr: %(err)r", { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend(str(e)) finally: try: os.remove(zone_tmp_fn) except OSError: pass
def delete_domain(self, context, domain): LOG.debug('Delete Domain') response, retry = self._make_and_send_dns_message( domain.name, self.timeout, CC, DELETE, CLASSCC, self.host, self.port) if response is None: raise exceptions.Backend()
def delete_zone(self, context, zone): LOG.debug('Delete Zone') response, retry = self._make_and_send_dns_message( zone.name, self.timeout, pcodes.CC, pcodes.DELETE, pcodes.CLASSCC, self.host, self.port) if response is None: raise exceptions.Backend("failed delete_zone()")
def gen_create_payload(self, zone, masters, contract_id, gid, tenant_id, target): if contract_id is None: raise exceptions.Backend( 'contractId is required for zone creation') masters = self.build_masters_field(masters) body = { 'zone': zone['name'], 'type': 'secondary', 'comment': 'Created by Designate for Tenant %s' % tenant_id, 'masters': masters, } # Add tsigKey if it exists if target.options.get('tsig_key_name'): # It's not mentioned in doc, but json schema supports specification # TsigKey in the same zone creation body body.update({'tsigKey': self.gen_tsig_payload(target)}) params = { 'contractId': contract_id, 'gid': gid, } return { 'url': 'config-dns/v2/zones', 'params': params, 'json': body, }
def delete_zone(self, context, zone): """Delete a DNS zone""" try: requests.delete(self._build_url(zone.name), headers=self.headers).raise_for_status() except requests.HTTPError as e: raise exceptions.Backend(e)
def wrapper(*a, **kw): try: return fn(*a, **kw) except exceptions.Backend: raise except Exception as e: LOG.error("Unhandled exception %s", e, exc_info=True) raise exceptions.Backend(str(e))
def wrapper(*a, **kw): try: return fn(*a, **kw) except exceptions.Backend as e: raise e except Exception as e: LOG.error(_LE("Unhandled exception %s"), e.message, exc_info=True) raise exceptions.Backend(e.message)
def wrap_backend_call(): """ Wraps backend calls, ensuring any exception raised is a Backend exception. """ try: yield except exceptions.Backend: raise except Exception as e: raise exceptions.Backend('Unknown backend failure: %r' % e)
def create_zone(self, payload): result = self.post(payload) # NOTE: ignore error about duplicate SZ in AKAMAI if result.status_code == 409 and result.reason == 'Conflict': LOG.info("Can't create zone %s because it already exists", payload['json']['zone']) elif not result.ok: json_res = result.json() raise exceptions.Backend('Zone creation failed due to: %s' % json_res['detail'])
def validate_deletion_is_complete(self, request_id): check_url = '/config-dns/v2/zones/delete-requests/%s' % request_id deleted = False attempt = 0 while not deleted and attempt < 10: result = self.get(check_url) deleted = result.json()['isComplete'] attempt += 1 time.sleep(1.0) if not deleted: raise exceptions.Backend('Zone was not deleted after %s attempts' % attempt)
def __init__(self, agent_service): """Configure the backend""" super(MSDNSBackend, self).__init__(agent_service) self._dnsutils = utilsfactory.get_dnsutils() masters = cfg.CONF['service:agent'].masters if not masters: raise exceptions.Backend("Missing agent AXFR masters") # Only ip addresses are needed self._masters = [ns.split(":")[0] for ns in masters] LOG.info("AXFR masters: %r", self._masters)
def delete_zone(self, context, zone): """Delete a DNS zone""" # First verify that the zone exists if self._check_zone_exists(zone): try: requests.delete(self._build_url(zone), headers=self.headers).raise_for_status() except requests.HTTPError as e: raise exceptions.Backend(e) else: LOG.warning( "Trying to delete zone %s but that zone is not " "present in the ns1 backend. Assuming success.", zone)
def create_zone(self, context, zone): """Create a DNS zone""" masters = [] for master in self.masters: host = master.host if netaddr.IPAddress(host).version == 6: host = '[%s]' % host masters.append('%s:%d' % (host, master.port)) data = { "name": zone.name, "kind": "slave", "masters": masters, } if self.tsigkey_name: data['slave_tsig_key_ids'] = [self.tsigkey_name] if self._check_zone_exists(zone): LOG.info( '%s exists on the server. Deleting zone before creation', zone ) try: self.delete_zone(context, zone) except exceptions.Backend: LOG.error('Could not delete pre-existing zone %s', zone) raise try: requests.post( self._build_url(), json=data, headers=self.headers ).raise_for_status() except requests.HTTPError as e: # check if the zone was actually created - even with errors pdns # will create the zone sometimes if self._check_zone_exists(zone): LOG.info("%s was created with an error. Deleting zone", zone) try: self.delete_zone(context, zone) except exceptions.Backend: LOG.error('Could not delete errored zone %s', zone) raise exceptions.Backend(e) self.mdns_api.notify_zone_changed( context, zone, self.host, self.port, self.timeout, self.retry_interval, self.max_retries, self.delay)
def _execute_rndc(self, rndc_op): """Execute rndc :param rndc_op: rndc arguments :type rndc_op: list :returns: None :raises: exceptions.Backend """ try: rndc_call = self._rndc_call_base + rndc_op LOG.debug('Executing RNDC call: %r', rndc_call) utils.execute(*rndc_call) except utils.processutils.ProcessExecutionError as e: raise exceptions.Backend(e)
def delete_zone(self, context, zone): """Delete a DNS zone""" headers = {"X-API-Key": self.api_token} try: requests.delete(self._build_url(zone.name), headers=headers).raise_for_status() except requests.HTTPError as e: if e.response.status_code == 404 or e.response.status_code == 422: LOG.warning("Trying to delete zone %s but that zone is not " "present in the pdns backend. Assuming success.") return raise exceptions.Backend(e)
def delete_zone(self, zone_name): # - try to delete with force=True # - if we get Forbidden error - try to delete it with Checks logic result = self.post(self.gen_delete_payload(zone_name, force=True)) if result.status_code == 403 and result.reason == 'Forbidden': result = self.post(self.gen_delete_payload(zone_name, force=False)) if result.ok: request_id = result.json().get('requestId') LOG.info('Run soft delete for zone (%s) and requestId (%s)', zone_name, request_id) if request_id is None: reason = 'requestId missed in response' raise exceptions.Backend( 'Zone deletion failed due to: %s' % reason) self.validate_deletion_is_complete(request_id) if not result.ok and result.status_code != 404: reason = result.json().get('detail') or result.json() raise exceptions.Backend('Zone deletion failed due to: %s' % reason)
def _check_conf(self): """Run gdnsd to check its configuration """ try: out, err = utils.execute( cfg.CONF[CFG_GROUP].gdnsd_cmd_name, '-D', '-x', 'checkconf', '-c', self._confdir_path, run_as_root=False, ) except ProcessExecutionError as e: LOG.error("Command output: %(out)r Stderr: %(err)r", { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend("Configuration check failed")
def _execute_rndc(self, rndc_op): """Execute rndc :param rndc_op: rndc arguments :type rndc_op: list :returns: None :raises: exceptions.Backend """ try: rndc_call = self._rndc_call_base + rndc_op LOG.debug('Executing RNDC call: %r with timeout %s', rndc_call, self._rndc_timeout) utils.execute(*rndc_call, timeout=self._rndc_timeout) except (utils.processutils.ProcessExecutionError, subprocess.TimeoutExpired) as e: raise exceptions.Backend(e)
def create_zone(self, context, zone): """Create a DNS zone""" masters = \ ['%s:%d' % (master.host, master.port) for master in self.masters] data = { "name": zone.name, "kind": "slave", "masters": masters, } headers = {"X-API-Key": self.api_token} try: requests.post(self._build_url(), json=data, headers=headers).raise_for_status() except requests.HTTPError as e: raise exceptions.Backend(e)
def create_zone(self, context, zone): master = self._get_master() # designate requires "." at end of zone name, NS1 requires omitting data = { "zone": zone.name.rstrip('.'), "secondary": { "enabled": True, "primary_ip": master.host, "primary_port": master.port } } if self.tsigkey_name: tsig = { "enabled": True, "hash": self.tsigkey_hash, "name": self.tsigkey_name, "key": self.tsigkey_value } data['secondary']['tsig'] = tsig if not self._check_zone_exists(zone): try: requests.put(self._build_url(zone), json=data, headers=self.headers).raise_for_status() except requests.HTTPError as e: # check if the zone was actually created if self._check_zone_exists(zone): LOG.info("%s was created with an error. Deleting zone", zone.name) try: self.delete_zone(context, zone) except exceptions.Backend: LOG.error('Could not delete errored zone %s', zone.name) raise exceptions.Backend(e) else: LOG.info("Can't create zone %s because it already exists", zone.name) self.mdns_api.notify_zone_changed(context, zone, self.host, self.port, self.timeout, self.retry_interval, self.max_retries, self.delay)
def __init__(self, *a, **kw): """Configure the backend""" super(DjbdnsBackend, self).__init__(*a, **kw) conf = cfg.CONF[CFG_GROUP_NAME] self._resolver = dns.resolver.Resolver(configure=False) self._resolver.timeout = SOA_QUERY_TIMEOUT self._resolver.lifetime = SOA_QUERY_TIMEOUT self._resolver.nameservers = [conf.query_destination] self._masters = [ utils.split_host_port(ns) for ns in cfg.CONF['service:agent'].masters ] LOG.info("Resolvers: %r", self._resolver.nameservers) LOG.info("AXFR masters: %r", self._masters) if not self._masters: raise exceptions.Backend("Missing agent AXFR masters") self._tcpclient_cmd_name = conf.tcpclient_cmd_name self._axfr_get_cmd_name = conf.axfr_get_cmd_name # Directory where data.cdb lives, usually /var/lib/djbdns/root tinydns_root_dir = os.path.join(conf.tinydns_datadir, 'root') # Usually /var/lib/djbdns/root/data.cdb self._tinydns_cdb_filename = os.path.join(tinydns_root_dir, 'data.cdb') LOG.info("data.cdb path: %r", self._tinydns_cdb_filename) # Where the agent puts the zone datafiles, # usually /var/lib/djbdns/datafiles self._datafiles_dir = datafiles_dir = os.path.join( conf.tinydns_datadir, 'datafiles') self._datafiles_tmp_path_tpl = os.path.join(datafiles_dir, "%s.ztmp") self._datafiles_path_tpl = os.path.join(datafiles_dir, "%s.zonedata") self._datafiles_path_glob = self._datafiles_path_tpl % '*' self._check_dirs(tinydns_root_dir, datafiles_dir)
def test_create_zone_raises_on_exception(self, mock_execute): mock_execute.side_effect = exceptions.Backend('badop') self.assertRaises(exceptions.Backend, self.backend.create_zone, self.admin_context, self.zone)
def test_delete_zone_already_deleted(self, mock_execute): mock_execute.side_effect = exceptions.Backend('not found') self.backend.delete_zone(self.admin_context, self.zone)