def record_state_in_domain(dns_record: DnsRecord, domain_records: list) -> RecordState: """Report if the record is missing or present, different or the same. First the record will be searched for, using filter_domain_records. Raises an exception if multiple records are found. As this script is not designed to handle this, it will raise an exception. The single record is tagged with the possible enumerations of the RecordState class NOTFOUND: The record is not present FOUND_SAME: Record is present and the content is (already) the same FOUND_DIFFERENT: Record is present, but with different content FOUND_NO_REQUEST_DATA: If the content of the (requested) dns_record is empty. This may occur when deleting a record (just) by name. Note on expire/TTL: To create/change/delete a record, type, content and TTL must all be present for each of the API calls. If the TTL was not provided on the commandline, the (only) )found record will be assumed to be the targeted record, therefore it's TTL will be entered in the dns_record if it is missing. :param dns_record: Record to search for in the domain list :type dns_record: DnsRecord :param domain_records: List of domain records :type domain_records: list :raises DuplicateDnsRecords: More the one record was found. :return: The state of the record in the domain list :rtype: RecordState """ record_list = filter_domain_records(domain_records, dns_record, ignore_content=True) if len(record_list) > 1: records_data = ", ".join([record["content"] for record in record_list]) raise DuplicateDnsRecords( ( f"Multiple records found for '{dns_record.fqdn}' " f"('{dns_record.rtype}'); '{records_data}'. " "Not processing as this may lead to unexpected results" ) ) if len(record_list) == 0: logger.info(f"Record '{dns_record.fqdn}', type '{dns_record.rtype}' not found!") return RecordState.NOTFOUND if len(record_list) == 1: if dns_record.expire is None: dns_record.expire = record_list[0]["expire"] if dns_record.content is None: dns_record.content = record_list[0]["content"] return RecordState.FOUND_NO_REQUEST_DATA if dns_record.content == record_list[0]["content"]: return RecordState.FOUND_SAME return RecordState.FOUND_DIFFERENT
def test_init(self, mocker, record_data: dict): """Test the DnsRecord class initialization. Args: mocker (pytest_mock.plugin.MockerFixture): mocking record_data (dict): Dictionary with data for the DnsRecord """ record_data = stl(record_data) if record_data["query_data"]: mocker.patch( "transip_dns.transip_interface.DnsRecord.query_for_content", return_value=record_data["query_data"], ) dns_record = DnsRecord(**record_data) if record_data["query_data"] is not None: # If query, test rdata separate that is has the query_result assert dns_record.content == record_data["query_data"] # rdata has been tested, take it out of the test del record_data["content"] # query_data is no attribute so remove it, but used in setting rdata del record_data["query_data"] for field in record_data: assert getattr(dns_record, field) == record_data[field] assert dns_record.fqdn == f"{dns_record.name}.{dns_record.zone}"
def process_parameters( args: argparse.Namespace, ) -> Tuple[TransipInterface, DnsRecord, List]: """Functionally process the parameters into usable objects. Provide a connection with TransIP, a record object of the targeted record and a domainlisting. :param args: the parsed arguments from command line and environment :type args: argparse.Namespace :return: [description] :rtype: Tuple[TransipInterface, DnsRecord, List] """ dns_record = DnsRecord( name=args.record_name, rtype=args.record_type, expire=args.record_ttl, content=args.record_data, zone=args.domainname, query_data=args.query_url, ) transip_interface = TransipInterface( login=args.user, private_key_pem=args.private_key, private_key_pem_file=args.private_key_file, access_token=args.token, global_key=True, ) domain_records = [] if args.domains is False: response = transip_interface.get_dns_entry(dns_zone_name=dns_record.zone) domain_records = response.json()["dnsEntries"] if dns_record.name is not None: # Can only occur when list of domain is requested dns_record.record_state = record_state_in_domain(dns_record, domain_records) return (transip_interface, dns_record, domain_records)
def delete_by_name_and_type( transip_interface, record_name: str, record_type: str = "A", raise_exception_if_missing: bool = False, raise_exception_if_found: bool = False, ): """Delete a specified record, if found. By arguments raise_exception_if_missing and raise_exception_if_found, a validation, assertion, can be performed to test expected outcomes. For example when doing a cleanup after a test has created records, then they they should actually be there. Args: transip_interface ([type]): [description] record_name (str): Name of the DNS record record_type (str, optional): DNS record type. Defaults to "A". raise_exception_if_missing (bool =False): raise exception if should be there raise_exception_if_found (bool =False): raise exception if should not be there Exception: Raise exception if an entry was supposed to be there """ # Destroy record, but should results in error as it should have been deleted if transip_interface._token == transip_demo_token: return # pragma: not live account skip live coverage else: # pragma: not demo account skip demo coverage record_missing = True for record in transip_interface.get_dns_entry( transip_domain).json()["dnsEntries"]: if record["name"] == record_name and record["type"] == record_type: record_missing = False transip_interface.delete_dns_entry( DnsRecord( zone=transip_domain, name=record_name, rtype=record_type, expire=record["expire"], content=record["content"], )) break if (raise_exception_if_missing and record_missing ): # pragma: code in case test and/or cleanup failed raise Exception( "{record_name}, {record_type}, not found for deletion") if (raise_exception_if_found and not record_missing ): # pragma: code in case test and/or cleanup failed raise Exception( "{record_name}, {record_type} found, should not be here anymore" )
def test_query_for_content(self, requests_mock): """Test query_ip function. Fairly simple test that the function will pass on the result of the url request Args: requests_mock (requests_mock.mocker.Mocker): a mock specifically for the url request """ query_url = "https://ipv4_or_ipv6_address" ip_address = "::ffff:198.51.100.1" requests_mock.get(query_url, text=ip_address) returned_ip = DnsRecord.query_for_content(query_url) assert returned_ip == ip_address
def test_init_errors(self, content, expire, name, rtype, error_expected, error_type): """Test if (in)correct record types are allowed/raised.""" if error_expected: pytest.raises(error_type, DnsRecord, name, rtype, expire, content, "x.net") else: entry = DnsRecord(name, rtype, expire, content, "x.net") assert entry.content == content assert entry.expire == expire assert entry.name == name assert entry.rtype == rtype
def create_record_of_each_type(request, record_data_for_each_record_type, transip_interface): """Loop over each RECORD_TYPEs and create such a record instance. Args: request (pytest.fixtures.SubRequest): Provides access to iteration in params; record types record_data_for_each_record_type (fixture/function): Hash of valid name/value pairs for a/each DNS record type transip_interface (fixture): Connection with TransIP Yields: dns_record [hash]: Command line parameters for the record to be created test_string [str]: Expected success string to be produced by the script """ record_type = request.param record = record_data_for_each_record_type(request.param) record_name = record["name"] record_data = record["value"] record_ttl = "300" dns_record_parameters = { "--record_type": record_type, "--record_name": record_name, "--record_data": record_data, "--record_ttl": record_ttl, } yield (dns_record_parameters, record) rec = DnsRecord( content=record_data, name=record_name, rtype=record_type, expire=record_ttl, zone=transip_domain, ) # Destroy record if transip_interface._token != transip_demo_token: transip_interface.delete_dns_entry( # pragma: not demo account skip demo coverage rec)
def test_execute_dns_retry( self, mocker, requests_mock, caplog, retries, negative_responses, method, ): dns_record = DnsRecord("name", "A", 300, "ip", "example.com") response_error = {"status_code": 409} response_ok = { "status_code": 204, "text": '{"dnsEntries": "whatever"}', } mocked_response = [] # Return a certain number of"negative_responses" for _ in range(negative_responses): mocked_response.append(response_error) # Before a positive response mocked_response.append(response_ok) # Dynamically mock requests.get, post, patch and delete request_mock_action = getattr(requests_mock, method) request_mock_action( "https://api.transip.nl/v6/domains/example.com/dns", mocked_response) transip_interface = TransipInterface( access_token="complex key", retry=retries, retry_delay=0.01, ) # Dynamically pick transip_interface.get_dns_entry, post, patch and delete transip_interface_test_dns_entry = getattr(transip_interface, f"{method}_dns_entry") # Post, patch and delete need the full record dns_parameter = dns_record if method == "get": # get only needs the zone to list dns_parameter = dns_record.zone caplog.set_level(logging.DEBUG) if negative_responses > retries: pytest.raises( requests.exceptions.HTTPError, transip_interface_test_dns_entry, dns_parameter, ) assert (len(re.findall(r"(API request returned 409)", caplog.text)) == retries + 1) else: response = transip_interface_test_dns_entry(dns_parameter) if method == "get": # get "unpacks" the response into actual content assert response.json()["dnsEntries"] == "whatever" assert response.status_code == 204 assert (len(re.findall(r"(API request returned 409)", caplog.text)) == negative_responses) assert len(re.findall(r"(API request returned 204)", caplog.text)) == 1
def delete_record_of_each_type(request, record_data_for_each_record_type, transip_interface): """Loop over each RECORD_TYPEs and delete such a record. Args: request (pytest.fixtures.SubRequest): Provides access to iteration in params; record types. record_data_for_each_record_type (fixture/function): Hash of valid name/value pairs for a/each DNS record type. transip_interface (fixture): Connection with TransIP. Yields: dns_record [hash]: Command line parameters for the record to be deleted. test_string [str]: Expected success string to be produced by the script. """ record_type = request.param record = record_data_for_each_record_type(record_type=request.param, for_create=False) record_name = record["name"] record_data = record["value"] record_ttl = "300" dns_record_parameters = { "--record_type": record_type, "--record_name": record_name, "--record_data": record_data, "--record_ttl": record_ttl, } dns_record_object = DnsRecord( name=record_name, rtype=record_type, expire=record_ttl, content=record_data, zone=transip_domain, ) try: transip_interface.post_dns_entry(dns_record_object) except requests.exceptions.HTTPError as e: # pragma: no cover if "this exact record already exists" in e.response.content.decode(): # Record might be left by previous (failed) attempt. # If tests run according to plan, this will not be executed pass partial_record = False if record["hide_ttl"]: del dns_record_parameters["--record_ttl"] partial_record = True if record["hide_value"]: del dns_record_parameters["--record_data"] partial_record = True dns_record_parameters["--delete"] = None yield (dns_record_parameters, dns_record_object, partial_record) # Destroy record, as the test should have delete all the records # the exception will never be triggered try: transip_interface.delete_dns_entry(dns_record_object) except Exception as e: if ( # pragma: no cover e.response.status_code == 404 and "Dns entry not found" in e.response.content.decode()): pass