def delete_dns_record(self, record): """ Delete a record from the domain. """ lexicon_config = self._get_base_config() lexicon_config['domain'] = record['domain'] lexicon_config['action'] = 'delete' lexicon_config['name'] = record['name'] lexicon_config['type'] = record['type'] config = ConfigResolver() config.with_dict(dict_object=lexicon_config) client = Client(config) result = False try: result = client.execute() # Invalidate cache for the domain-cname pair cache.delete(f"{record['domain']}-{record['type']}") except Exception as e: # pylint: disable=broad-except # This ugly checking of the exception message is needed # as the library only throws an instance of the Exception class. if 'Record identifier could not be found' in str(e): result = True else: raise return result
def main() -> None: """Main function of Lexicon.""" # Dynamically determine all the providers available and gather command line arguments. parsed_args = generate_cli_main_parser().parse_args() log_level = logging.getLevelName(parsed_args.log_level) logging.basicConfig(stream=sys.stdout, level=log_level, format="%(message)s") logger.debug("Arguments: %s", parsed_args) # In the CLI context, will get configuration interactively: # * from the command line # * from the environment variables # * from lexicon configuration files found in given --config-dir (default is current dir) config = ConfigResolver() config.with_args(parsed_args).with_env().with_config_dir(parsed_args.config_dir) client = Client(config) results = client.execute() action = config.resolve("lexicon:action") if not action: raise ValueError("Parameter action is not set.") handle_output(results, parsed_args.output, action)
def _txt_challenge( profile: Dict[str, Any], token: str, domain: str, action: str = "create", ): profile_name = profile["name"] provider_name = profile["provider"] provider_options = profile.get("provider_options", {}) if not provider_options: print(f"No provider_options are defined for profile {profile_name}, " "any call to the provider API is likely to fail.") config_dict = { "action": action, "domain": domain, "type": "TXT", "name": "_acme-challenge.{0}.".format(domain), "content": token, "delegated": profile.get("delegated_subdomain"), "provider_name": provider_name, provider_name: provider_options, } ttl = profile.get("ttl") if ttl: config_dict["ttl"] = ttl lexicon_config = ConfigResolver() lexicon_config.with_dict(config_dict) Client(lexicon_config).execute()
def test_dict_resolution(): dict_object = {"delegated": "TEST1", "cloudflare": {"auth_token": "TEST2"}} config = ConfigResolver().with_dict(dict_object) assert config.resolve("lexicon:delegated") == "TEST1" assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2" assert config.resolve("lexicon:nonexistent") is None
def test_dict_resolution(): dict_object = {'delegated': 'TEST1', 'cloudflare': {'auth_token': 'TEST2'}} config = ConfigResolver().with_dict(dict_object) assert config.resolve('lexicon:delegated') == 'TEST1' assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2' assert config.resolve('lexicon:nonexistent') is None
def test_config_lexicon_file_resolution(tmpdir): lexicon_file = tmpdir.join("lexicon.yml") lexicon_file.write("delegated: TEST1\ncloudflare:\n auth_token: TEST2") config = ConfigResolver().with_config_file(str(lexicon_file)) assert config.resolve("lexicon:delegated") == "TEST1" assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2" assert config.resolve("lexicon:nonexistent") is None
def test_provider_config_lexicon_file_resolution(tmpdir): provider_file = tmpdir.join("lexicon_cloudflare.yml") provider_file.write("auth_token: TEST2") config = ConfigResolver().with_provider_config_file( "cloudflare", str(provider_file)) assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2" assert config.resolve("lexicon:nonexistent") is None
def test_generic_config_feeder_resolution(): class GenericConfigSource(ConfigSource): def resolve(self, config_key): return 'TEST1' config = ConfigResolver().with_config_source(GenericConfigSource()) assert config.resolve('lexicon:cloudflare:auth_username') == 'TEST1' assert config.resolve('lexicon:nonexistent') == 'TEST1'
def test_config_lexicon_file_resolution(tmpdir): lexicon_file = tmpdir.join('lexicon.yml') lexicon_file.write('delegated: TEST1\ncloudflare:\n auth_token: TEST2') config = ConfigResolver().with_config_file(str(lexicon_file)) assert config.resolve('lexicon:delegated') == 'TEST1' assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2' assert config.resolve('lexicon:nonexistent') is None
def test_provider_config_lexicon_file_resolution(tmpdir): provider_file = tmpdir.join('lexicon_cloudflare.yml') provider_file.write('auth_token: TEST2') config = ConfigResolver().with_provider_config_file( 'cloudflare', str(provider_file)) assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2' assert config.resolve('lexicon:nonexistent') is None
def test_prioritized_resolution(tmpdir, monkeypatch): lexicon_file = tmpdir.join("lexicon.yml") lexicon_file.write("cloudflare:\n auth_token: TEST1") monkeypatch.setenv("LEXICON_CLOUDFLARE_AUTH_TOKEN", "TEST2") assert (ConfigResolver().with_config_file( str(lexicon_file)).with_env().resolve("lexicon:cloudflare:auth_token") == "TEST1") assert (ConfigResolver().with_env().with_config_file( str(lexicon_file)).resolve("lexicon:cloudflare:auth_token") == "TEST2")
def test_prioritized_resolution(tmpdir, monkeypatch): lexicon_file = tmpdir.join('lexicon.yml') lexicon_file.write('cloudflare:\n auth_token: TEST1') monkeypatch.setenv('LEXICON_CLOUDFLARE_AUTH_TOKEN', 'TEST2') assert ConfigResolver().with_config_file( str(lexicon_file)).with_env().resolve( 'lexicon:cloudflare:auth_token') == 'TEST1' assert ConfigResolver().with_env().with_config_file( str(lexicon_file)).resolve('lexicon:cloudflare:auth_token') == 'TEST2'
def _create_client(self, zone_name): config = LexiconConfigResolver() dynamic_config = OnTheFlyLexiconConfigSource(zone_name) config.with_config_source(dynamic_config) \ .with_env().with_dict(self.lexicon_config) try: return LexiconClient(config), dynamic_config except AttributeError as e: self.log.error('Unable to parse config {!s}'.format(config)) raise e
def test_argparse_resolution(): parser = generate_cli_main_parser() data = parser.parse_args([ '--delegated', 'TEST1', 'cloudflare', 'create', 'example.com', 'TXT', '--auth-token', 'TEST2' ]) config = ConfigResolver().with_args(data) assert config.resolve('lexicon:delegated') == 'TEST1' assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2' assert config.resolve('lexicon:nonexistent') is None
def txt_challenge( certificate: Dict[str, Any], profile: Dict[str, Any], token: str, domain: str, action: str = "create", ): profile_name = profile["name"] provider_name = profile["provider"] provider_options = profile.get("provider_options", {}) if not provider_options: print(f"No provider_options are defined for profile {profile_name}, " "any call to the provider API is likely to fail.") challenge_name = f"_acme-challenge.{domain}." if certificate.get("follow_cnames"): print( f"Trying to resolve the canonical challenge name for {challenge_name}" ) canonical_challenge_name = resolve_canonical_challenge_name( challenge_name) print( f"Canonical challenge name found for {challenge_name}: {canonical_challenge_name}" ) challenge_name = canonical_challenge_name extracted = tldextract.extract(challenge_name) domain = ".".join([extracted.domain, extracted.suffix]) config_dict = { "action": action, "domain": domain, "type": "TXT", "name": challenge_name, "content": token, "delegated": profile.get("delegated_subdomain"), "provider_name": provider_name, provider_name: provider_options, } ttl = profile.get("ttl") if ttl: config_dict["ttl"] = ttl lexicon_config = ConfigResolver() lexicon_config.with_dict(config_dict) Client(lexicon_config).execute()
def test_delete_action_is_correctly_handled_by_provider( capsys, lexicon_client): client = lexicon_client.Client(ConfigResolver().with_dict({ "action": "delete", "provider_name": "fakeprovider", "domain": "example.com", "identifier": "fake-id", "type": "TXT", "name": "fake", "content": "fake-content", })) results = client.execute() out, _ = capsys.readouterr() assert "Authenticate action" in out assert results["action"] == "delete" assert results["domain"] == "example.com" assert results["identifier"] == "fake-id" assert results["type"] == "TXT" assert results["name"] == "fake" assert results["content"] == "fake-content"
def add_dns_record(self, record): """ Add a DNS record to the domain. """ lexicon_config = self._get_base_config() lexicon_config['domain'] = record['domain'] lexicon_config['action'] = 'create' lexicon_config['type'] = record['type'] lexicon_config['name'] = record['name'] lexicon_config['content'] = record['value'] lexicon_config['ttl'] = record['ttl'] config = ConfigResolver() config.with_dict(dict_object=lexicon_config) client = Client(config) result = client.execute() return result
def build_lexicon_config( lexicon_provider_name: str, lexicon_options: Mapping[str, Any], provider_options: Mapping[str, Any]) -> Union[ConfigResolver, Dict[str, Any]]: """ Convenient function to build a Lexicon 2.x/3.x config object. :param str lexicon_provider_name: the name of the lexicon provider to use :param dict lexicon_options: options specific to lexicon :param dict provider_options: options specific to provider :return: configuration to apply to the provider :rtype: ConfigurationResolver or dict """ config: Union[ConfigResolver, Dict[str, Any]] = { 'provider_name': lexicon_provider_name } config.update(lexicon_options) if not ConfigResolver: # Lexicon 2.x config.update(provider_options) else: # Lexicon 3.x provider_config: Dict[str, Any] = {} provider_config.update(provider_options) config[lexicon_provider_name] = provider_config config = ConfigResolver().with_dict(config).with_env() return config
def test_client_init_when_missing_type_should_fail(): options = { "provider_name": "fakeprovider", "action": "list", "domain": "example.com", } with pytest.raises(AttributeError): lexicon.client.Client(ConfigResolver().with_dict(options))
def test_argparse_resolution(): parser = generate_cli_main_parser() data = parser.parse_args([ "--delegated", "TEST1", "cloudflare", "create", "example.com", "TXT", "--auth-token", "TEST2", ]) config = ConfigResolver().with_args(data) assert config.resolve("lexicon:delegated") == "TEST1" assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2" assert config.resolve("lexicon:nonexistent") is None
def test_missing_optional_client_config_parameter_does_not_raise_error( lexicon_client): lexicon_client.Client(ConfigResolver().with_dict({ "action": "list", "provider_name": "fakeprovider", "domain": "example.com", "type": "TXT", "no-name": "fake", "no-content": "fake", }))
def test_unknown_provider_raises_error(lexicon_client): with pytest.raises(ProviderNotAvailableError): lexicon_client.Client(ConfigResolver().with_dict({ "action": "list", "provider_name": "unknownprovider", "domain": "example.com", "type": "TXT", "name": "fake", "content": "fake", }))
def authenticate(self): """ Launch the authentication process: for 'auto' provider, it means first to find the relevant provider, then call its authenticate() method. Almost every subsequent operation will then be delegated to that provider. """ mapping_override = self.config.resolve('lexicon:auto:mapping_override') print(mapping_override) mapping_override_processed = {} if mapping_override: for one_mapping in mapping_override.split(','): one_mapping_processed = one_mapping.split(':') mapping_override_processed[ one_mapping_processed[0]] = one_mapping_processed[1] override_provider = mapping_override_processed.get(self.domain) if override_provider: provider = [ element for element in AVAILABLE_PROVIDERS.items() if element[0] == override_provider ][0] LOGGER.info('Provider authoritatively mapped for domain %s: %s.', self.domain, provider.__name__) (provider_name, provider_module) = provider else: (provider_name, provider_module) = _relevant_provider_for_domain(self.domain) LOGGER.info('Provider discovered for domain %s: %s.', self.domain, provider_name) new_config = ConfigResolver() new_config.with_dict({'lexicon:provider_name': provider_name}) target_prefix = 'auto_{0}_'.format(provider_name) for configSource in self.config._config_sources: if not isinstance(configSource, ArgsConfigSource): new_config.with_config_source(configSource) else: # ArgsConfigSource needs to be reprocessed to rescope the provided # args to the delegate provider new_dict = {} for key, value in configSource._parameters.items(): if key.startswith(target_prefix): new_param_name = re.sub('^{0}'.format(target_prefix), '', key) new_dict['lexicon:{0}:{1}'.format( provider_name, new_param_name)] = value elif not key.startswith('auto_'): new_dict['lexicon:{0}'.format(key)] = value new_config.with_dict(new_dict) self.proxy_provider = provider_module.Provider(new_config) self.proxy_provider.authenticate()
def list_dns_records(self, record): """ List all records of a domain name for a given type. """ cached_result = cache.get(f"{record['domain']}-{record['type']}") if cached_result: return cached_result lexicon_config = self._get_base_config() lexicon_config['domain'] = record['domain'] lexicon_config['action'] = 'list' lexicon_config['type'] = record['type'] config = ConfigResolver() config.with_dict(dict_object=lexicon_config) client = Client(config) result = client.execute() cache.set(f"{record['domain']}-{record['type']}", result) return result
def test_client_init_when_domain_includes_subdomain_should_strip(): options = { "provider_name": "fakeprovider", "action": "list", "domain": "www.example.com", "type": "TXT", } client = lexicon.client.Client(ConfigResolver().with_dict(options)) assert client.provider_name == options["provider_name"] assert client.action == options["action"] assert client.config.resolve("lexicon:domain") == "example.com" assert client.config.resolve("lexicon:type") == options["type"]
def test_client_basic_init(): options = { "provider_name": "fakeprovider", "action": "list", "domain": "example.com", "type": "TXT", } client = lexicon.client.Client(ConfigResolver().with_dict(options)) assert client.provider_name == options["provider_name"] assert client.action == options["action"] assert client.config.resolve("lexicon:domain") == options["domain"] assert client.config.resolve("lexicon:type") == options["type"]
def test_Client_basic_init(): options = { 'provider_name': 'base', 'action': 'list', 'domain': 'example.com', 'type': 'TXT' } client = lexicon.client.Client(ConfigResolver().with_dict(options)) assert client.provider_name == options['provider_name'] assert client.action == options['action'] assert client.config.resolve('lexicon:domain') == options['domain'] assert client.config.resolve('lexicon:type') == options['type']
def test_client_init_with_same_delegated_domain_fqdn(): options = { "provider_name": "fakeprovider", "action": "list", "domain": "www.example.com", "delegated": "example.com", "type": "TXT", } client = lexicon.client.Client(ConfigResolver().with_dict(options)) assert client.provider_name == options["provider_name"] assert client.action == options["action"] assert client.config.resolve("lexicon:domain") == "example.com" assert client.config.resolve("lexicon:type") == options["type"]
def main(): """Main function of Lexicon.""" # Dynamically determine all the providers available and gather command line arguments. parsed_args = generate_cli_main_parser().parse_args() log_level = logging.getLevelName(parsed_args.log_level) logging.basicConfig(stream=sys.stdout, level=log_level, format='%(message)s') logger.debug('Arguments: %s', parsed_args) # In the CLI context, will get configuration interactively: # * from the command line # * from the environment variables # * from lexicon configuration files in working directory config = ConfigResolver() config.with_args(parsed_args).with_env().with_config_dir(os.getcwd()) client = Client(config) results = client.execute() handle_output(results, parsed_args.output)
def test_missing_required_client_config_parameter_raises_error(lexicon_client): with pytest.raises(AttributeError): lexicon_client.Client(ConfigResolver().with_dict({ "no-action": "list", "provider_name": "fakeprovider", "domain": "example.com", "type": "TXT", "name": "fake", "content": "fake", })) with pytest.raises(AttributeError): lexicon_client.Client(ConfigResolver().with_dict({ "action": "list", "no-provider_name": "fakeprovider", "domain": "example.com", "type": "TXT", "name": "fake", "content": "fake", })) with pytest.raises(AttributeError): lexicon_client.Client(ConfigResolver().with_dict({ "action": "list", "provider_name": "fakeprovider", "no-domain": "example.com", "type": "TXT", "name": "fake", "content": "fake", })) with pytest.raises(AttributeError): lexicon_client.Client(ConfigResolver().with_dict({ "action": "list", "provider_name": "fakeprovider", "domain": "example.com", "no-type": "TXT", "name": "fake", "content": "fake", }))