mirror of
https://github.com/certbot/certbot.git
synced 2026-06-03 22:08:07 -04:00
Refactor Lexicon-based DNS plugins (#9746)
* Refactor Lexicon-based DNS plugins and upgrade minimal version of Lexicon * Relax filterwarning to comply with envs where boto3 is not installed * Update pinned dependencies * Use our previous method to deprecate part of modules * Safe import internally * Add changelog Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
parent
694c758db7
commit
732a3ac962
33 changed files with 542 additions and 669 deletions
|
|
@ -2,34 +2,30 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import dnsimple
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://dnsimple.com/user'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for DNSimple
|
||||
|
||||
This Authenticator uses the DNSimple v2 API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('token',
|
||||
f'User access token for DNSimple v2 API. (See {ACCOUNT_URL}.)',
|
||||
'auth_token')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -41,42 +37,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the DNSimple API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'DNSimple credentials INI file',
|
||||
{
|
||||
'token': 'User access token for DNSimple v2 API. (See {0}.)'.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_dnsimple_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_dnsimple_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_dnsimple_client(self) -> "_DNSimpleLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _DNSimpleLexiconClient(cast(str, self.credentials.conf('token')), self.ttl)
|
||||
|
||||
|
||||
class _DNSimpleLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the DNSimple via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, token: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('dnssimple', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_token': token,
|
||||
})
|
||||
|
||||
self.provider = dnsimple.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'dnssimple'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:
|
||||
hint = None
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""Tests for certbot_dns_dnsimple._internal.dns_dnsimple."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import HTTPError
|
||||
|
|
@ -16,7 +14,9 @@ TOKEN = 'foo'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: ...')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -31,23 +31,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "dnsimple")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_dnsimple_client | pylint: disable=protected-access
|
||||
self.auth._get_dnsimple_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class DNSimpleLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: ...')
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_dnsimple._internal.dns_dnsimple import _DNSimpleLexiconClient
|
||||
|
||||
self.client = _DNSimpleLexiconClient(TOKEN, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ version = '2.7.0.dev0'
|
|||
install_requires = [
|
||||
# This version of lexicon is required to address the problem described in
|
||||
# https://github.com/AnalogJ/lexicon/issues/387.
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import dnsmadeeasy
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://cp.dnsmadeeasy.com/account/info'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for DNS Made Easy
|
||||
|
||||
This Authenticator uses the DNS Made Easy API to fulfill a dns-01 challenge.
|
||||
|
|
@ -26,11 +22,17 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
|
||||
description = ('Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for '
|
||||
'DNS).')
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('api-key',
|
||||
'API key for DNS Made Easy account, '
|
||||
f'obtained from {ACCOUNT_URL}',
|
||||
'auth_username')
|
||||
self._add_provider_option('secret-key',
|
||||
'Secret key for DNS Made Easy account, '
|
||||
f'obtained from {ACCOUNT_URL}',
|
||||
'auth_token')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -42,48 +44,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the DNS Made Easy API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'DNS Made Easy credentials INI file',
|
||||
{
|
||||
'api-key': 'API key for DNS Made Easy account, obtained from {0}'
|
||||
.format(ACCOUNT_URL),
|
||||
'secret-key': 'Secret key for DNS Made Easy account, obtained from {0}'
|
||||
.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_dnsmadeeasy_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_dnsmadeeasy_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_dnsmadeeasy_client(self) -> "_DNSMadeEasyLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _DNSMadeEasyLexiconClient(cast(str, self.credentials.conf('api-key')),
|
||||
cast(str, self.credentials.conf('secret-key')),
|
||||
self.ttl)
|
||||
|
||||
|
||||
class _DNSMadeEasyLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the DNS Made Easy via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, secret_key: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('dnsmadeeasy', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_username': api_key,
|
||||
'auth_token': secret_key,
|
||||
})
|
||||
|
||||
self.provider = dnsmadeeasy.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'dnsmadeeasy'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if domain_name in str(e) and str(e).startswith('404 Client Error: Not Found for url:'):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""Tests for certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
|
@ -18,7 +17,10 @@ SECRET_KEY = 'bar'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.')
|
||||
LOGIN_ERROR = HTTPError(f'403 Client Error: Forbidden for url: {DOMAIN}.')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -35,24 +37,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "dnsmadeeasy")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_dnsmadeeasy_client | pylint: disable=protected-access
|
||||
self.auth._get_dnsmadeeasy_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class DNSMadeEasyLexiconClientTest(unittest.TestCase,
|
||||
dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
|
||||
LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: {0}.'.format(DOMAIN))
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy import _DNSMadeEasyLexiconClient
|
||||
|
||||
self.client = _DNSMadeEasyLexiconClient(API_KEY, SECRET_KEY, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import gehirn
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DASHBOARD_URL = "https://gis.gehirn.jp/"
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for Gehirn Infrastructure Service DNS
|
||||
|
||||
This Authenticator uses the Gehirn Infrastructure Service API to fulfill
|
||||
|
|
@ -27,11 +23,17 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
|
||||
description = 'Obtain certificates using a DNS TXT record ' + \
|
||||
'(if you are using Gehirn Infrastructure Service for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('api-token',
|
||||
'API token for Gehirn Infrastructure Service '
|
||||
f'API obtained from {DASHBOARD_URL}',
|
||||
'auth_token')
|
||||
self._add_provider_option('api-secret',
|
||||
'API secret for Gehirn Infrastructure Service '
|
||||
f'API obtained from {DASHBOARD_URL}',
|
||||
'auth_secret')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -43,50 +45,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Gehirn Infrastructure Service API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'Gehirn Infrastructure Service credentials file',
|
||||
{
|
||||
'api-token': 'API token for Gehirn Infrastructure Service ' + \
|
||||
'API obtained from {0}'.format(DASHBOARD_URL),
|
||||
'api-secret': 'API secret for Gehirn Infrastructure Service ' + \
|
||||
'API obtained from {0}'.format(DASHBOARD_URL),
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_gehirn_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_gehirn_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_gehirn_client(self) -> "_GehirnLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _GehirnLexiconClient(
|
||||
cast(str, self.credentials.conf('api-token')),
|
||||
cast(str, self.credentials.conf('api-secret')),
|
||||
self.ttl
|
||||
)
|
||||
|
||||
|
||||
class _GehirnLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the Gehirn Infrastructure Service via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_token: str, api_secret: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('gehirn', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_token': api_token,
|
||||
'auth_secret': api_secret,
|
||||
})
|
||||
|
||||
self.provider = gehirn.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'gehirn'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')):
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@ from certbot.tests import util as test_util
|
|||
API_TOKEN = '00000000-0000-0000-0000-000000000000'
|
||||
API_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.')
|
||||
LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -35,23 +39,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "gehirn")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_gehirn_client | pylint: disable=protected-access
|
||||
self.auth._get_gehirn_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class GehirnLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN))
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_gehirn._internal.dns_gehirn import _GehirnLexiconClient
|
||||
|
||||
self.client = _GehirnLexiconClient(API_TOKEN, API_SECRET, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,8 @@ from typing import cast
|
|||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from lexicon.providers import linode
|
||||
from lexicon.providers import linode4
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -21,7 +16,7 @@ API_KEY_URL = 'https://manager.linode.com/profile/api'
|
|||
API_KEY_URL_V4 = 'https://cloud.linode.com/profile/tokens'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for Linode
|
||||
|
||||
This Authenticator uses the Linode API to fulfill a dns-01 challenge.
|
||||
|
|
@ -31,7 +26,10 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('key',
|
||||
'API key for Linode account, '
|
||||
f'obtained from {API_KEY_URL} or {API_KEY_URL_V4}',
|
||||
'auth_token')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -43,29 +41,13 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Linode API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'Linode credentials INI file',
|
||||
{
|
||||
'key': 'API key for Linode account, obtained from {0} or {1}'
|
||||
.format(API_KEY_URL, API_KEY_URL_V4)
|
||||
}
|
||||
)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
if not hasattr(self, '_credentials'): # pragma: no cover
|
||||
self._setup_credentials()
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_linode_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_linode_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_linode_client(self) -> '_LinodeLexiconClient':
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
api_key = cast(str, self.credentials.conf('key'))
|
||||
api_version: Optional[Union[str, int]] = self.credentials.conf('version')
|
||||
if api_version == '':
|
||||
api_version = None
|
||||
api_key = cast(str, self._credentials.conf('key'))
|
||||
api_version: Optional[Union[str, int]] = self._credentials.conf('version')
|
||||
|
||||
if not api_version:
|
||||
api_version = 3
|
||||
|
|
@ -78,34 +60,19 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
else:
|
||||
api_version = int(api_version)
|
||||
|
||||
return _LinodeLexiconClient(api_key, api_version)
|
||||
|
||||
|
||||
class _LinodeLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the Linode API.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, api_version: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.api_version = api_version
|
||||
|
||||
if api_version == 3:
|
||||
config = dns_common_lexicon.build_lexicon_config('linode', {}, {
|
||||
'auth_token': api_key,
|
||||
})
|
||||
|
||||
self.provider = linode.Provider(config)
|
||||
return 'linode'
|
||||
elif api_version == 4:
|
||||
config = dns_common_lexicon.build_lexicon_config('linode4', {}, {
|
||||
'auth_token': api_key,
|
||||
})
|
||||
return 'linode4'
|
||||
|
||||
self.provider = linode4.Provider(config)
|
||||
else:
|
||||
raise errors.PluginError('Invalid api version specified: {0}. (Supported: 3, 4)'
|
||||
.format(api_version))
|
||||
raise errors.PluginError(f'Invalid api version specified: {api_version}. (Supported: 3, 4)')
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self._credentials = self._configure_credentials(
|
||||
key='credentials',
|
||||
label='Credentials INI file for linode DNS authenticator',
|
||||
required_variables={item[0]: item[1] for item in self._provider_options},
|
||||
)
|
||||
|
||||
def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if not str(e).startswith('Domain not found'):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ TOKEN_V4 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('Domain not found')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -32,10 +34,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "linode")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_linode_client | pylint: disable=protected-access
|
||||
self.auth._get_linode_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_3_detection(self):
|
||||
path = os.path.join(self.tempdir, 'file_3_auto.ini')
|
||||
|
|
@ -44,9 +42,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 3 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_4_detection(self):
|
||||
|
|
@ -56,9 +53,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 4 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode4"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_3_detection_empty_version(self):
|
||||
|
|
@ -68,9 +64,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 3 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_4_detection_empty_version(self):
|
||||
|
|
@ -80,9 +75,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 4 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode4"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_3_manual(self):
|
||||
|
|
@ -92,9 +86,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 3 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_4_manual(self):
|
||||
|
|
@ -104,9 +97,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
client = auth._get_linode_client()
|
||||
assert 4 == client.api_version
|
||||
|
||||
assert auth._provider_name == "linode4"
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def test_api_version_error(self):
|
||||
|
|
@ -116,35 +108,9 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
config = mock.MagicMock(linode_credentials=path,
|
||||
linode_propagation_seconds=0)
|
||||
auth = Authenticator(config, "linode")
|
||||
auth._setup_credentials()
|
||||
|
||||
with pytest.raises(errors.PluginError):
|
||||
auth._get_linode_client()
|
||||
|
||||
|
||||
class LinodeLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('Domain not found')
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_linode._internal.dns_linode import _LinodeLexiconClient
|
||||
|
||||
self.client = _LinodeLexiconClient(TOKEN, 3)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
class Linode4LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('Domain not found')
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_linode._internal.dns_linode import _LinodeLexiconClient
|
||||
|
||||
self.client = _LinodeLexiconClient(TOKEN, 4)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
assert auth._provider_name == "linode4"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,34 +2,33 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import luadns
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://api.luadns.com/settings'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for LuaDNS
|
||||
|
||||
This Authenticator uses the LuaDNS API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('email',
|
||||
'email address associated with LuaDNS account',
|
||||
'auth_username')
|
||||
self._add_provider_option('token',
|
||||
f'API token for LuaDNS account, obtained from {ACCOUNT_URL}',
|
||||
'auth_token')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -41,46 +40,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the LuaDNS API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'LuaDNS credentials INI file',
|
||||
{
|
||||
'email': 'email address associated with LuaDNS account',
|
||||
'token': 'API token for LuaDNS account, obtained from {0}'.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_luadns_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_luadns_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_luadns_client(self) -> "_LuaDNSLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _LuaDNSLexiconClient(cast(str, self.credentials.conf('email')),
|
||||
cast(str, self.credentials.conf('token')),
|
||||
self.ttl)
|
||||
|
||||
|
||||
class _LuaDNSLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the LuaDNS via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, email: str, token: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('luadns', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_username': email,
|
||||
'auth_token': token,
|
||||
})
|
||||
|
||||
self.provider = luadns.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'luadns'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:
|
||||
hint = None
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for certbot_dns_luadns._internal.dns_luadns."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
|
@ -17,7 +15,9 @@ TOKEN = 'foo'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
LOGIN_ERROR = HTTPError("401 Client Error: Unauthorized for url: ...")
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -32,23 +32,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "luadns")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_luadns_client | pylint: disable=protected-access
|
||||
self.auth._get_luadns_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class LuaDNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
LOGIN_ERROR = HTTPError("401 Client Error: Unauthorized for url: ...")
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_luadns._internal.dns_luadns import _LuaDNSLexiconClient
|
||||
|
||||
self.client = _LuaDNSLexiconClient(EMAIL, TOKEN, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,34 +2,31 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import nsone
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://my.nsone.net/#/account/settings'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for NS1
|
||||
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""
|
||||
DNS Authenticator for NS1
|
||||
This Authenticator uses the NS1 API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certificates using a DNS TXT record (if you are using NS1 for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('api-key',
|
||||
f'API key for NS1 API, obtained from {ACCOUNT_URL}',
|
||||
'auth_token')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -41,42 +38,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the NS1 API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'NS1 credentials file',
|
||||
{
|
||||
'api-key': 'API key for NS1 API, obtained from {0}'.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_nsone_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_nsone_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_nsone_client(self) -> "_NS1LexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _NS1LexiconClient(cast(str, self.credentials.conf('api-key')), self.ttl)
|
||||
|
||||
|
||||
class _NS1LexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the NS1 via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('nsone', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_token': api_key,
|
||||
})
|
||||
|
||||
self.provider = nsone.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'nsone'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:') or
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for certbot_dns_nsone._internal.dns_nsone."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
|
@ -17,7 +15,10 @@ API_KEY = 'foo'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.')
|
||||
LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -32,23 +33,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "nsone")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_nsone_client | pylint: disable=protected-access
|
||||
self.auth._get_nsone_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class NS1LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN))
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_nsone._internal.dns_nsone import _NS1LexiconClient
|
||||
|
||||
self.client = _NS1LexiconClient(API_KEY, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,34 +2,40 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import ovh
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TOKEN_URL = 'https://eu.api.ovh.com/createToken/ or https://ca.api.ovh.com/createToken/'
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for OVH
|
||||
|
||||
This Authenticator uses the OVH API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certificates using a DNS TXT record (if you are using OVH for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('endpoint',
|
||||
'OVH API endpoint (ovh-eu or ovh-ca)',
|
||||
'auth_entrypoint')
|
||||
self._add_provider_option('application-key',
|
||||
f'Application key for OVH API, obtained from {TOKEN_URL}',
|
||||
'auth_application_key')
|
||||
self._add_provider_option('application-secret',
|
||||
f'Application secret for OVH API, obtained from {TOKEN_URL}',
|
||||
'auth_application_secret')
|
||||
self._add_provider_option('consumer-key',
|
||||
f'Consumer key for OVH API, obtained from {TOKEN_URL}',
|
||||
'auth_consumer_key')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -41,58 +47,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the OVH API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'OVH credentials INI file',
|
||||
{
|
||||
'endpoint': 'OVH API endpoint (ovh-eu or ovh-ca)',
|
||||
'application-key': 'Application key for OVH API, obtained from {0}'
|
||||
.format(TOKEN_URL),
|
||||
'application-secret': 'Application secret for OVH API, obtained from {0}'
|
||||
.format(TOKEN_URL),
|
||||
'consumer-key': 'Consumer key for OVH API, obtained from {0}'
|
||||
.format(TOKEN_URL),
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_ovh_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_ovh_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_ovh_client(self) -> "_OVHLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _OVHLexiconClient(
|
||||
cast(str, self.credentials.conf('endpoint')),
|
||||
cast(str, self.credentials.conf('application-key')),
|
||||
cast(str, self.credentials.conf('application-secret')),
|
||||
cast(str, self.credentials.conf('consumer-key')),
|
||||
self.ttl
|
||||
)
|
||||
|
||||
|
||||
class _OVHLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the OVH API via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint: str, application_key: str, application_secret: str,
|
||||
consumer_key: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('ovh', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_entrypoint': endpoint,
|
||||
'auth_application_key': application_key,
|
||||
'auth_application_secret': application_secret,
|
||||
'auth_consumer_key': consumer_key,
|
||||
})
|
||||
|
||||
self.provider = ovh.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'ovh'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:
|
||||
hint = None
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""Tests for certbot_dns_ovh._internal.dns_ovh."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import HTTPError
|
||||
|
|
@ -19,7 +17,10 @@ CONSUMER_KEY = 'spam'
|
|||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('Domain example.com not found')
|
||||
LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: https://eu.api.ovh.com/1.0/...')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -38,26 +39,7 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
self.config = mock.MagicMock(ovh_credentials=path,
|
||||
ovh_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "ovh")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_ovh_client | pylint: disable=protected-access
|
||||
self.auth._get_ovh_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class OVHLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = Exception('Domain example.com not found')
|
||||
LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: https://eu.api.ovh.com/1.0/...')
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_ovh._internal.dns_ovh import _OVHLexiconClient
|
||||
|
||||
self.client = _OVHLexiconClient(
|
||||
ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY, 0
|
||||
)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
self.auth = Authenticator(self.config, 'ovh')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
|
||||
from lexicon.providers import sakuracloud
|
||||
from requests import HTTPError
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins.dns_common import CredentialsConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APIKEY_URL = "https://secure.sakura.ad.jp/cloud/#!/apikey/top/"
|
||||
|
||||
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
class Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):
|
||||
"""DNS Authenticator for Sakura Cloud DNS
|
||||
|
||||
This Authenticator uses the Sakura Cloud API to fulfill a dns-01 challenge.
|
||||
|
|
@ -26,11 +22,15 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
|
||||
description = 'Obtain certificates using a DNS TXT record ' + \
|
||||
'(if you are using Sakura Cloud for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.credentials: Optional[CredentialsConfiguration] = None
|
||||
self._add_provider_option('api-token',
|
||||
f'API token for Sakura Cloud API obtained from {APIKEY_URL}',
|
||||
'auth_token')
|
||||
self._add_provider_option('api-secret',
|
||||
f'API secret for Sakura Cloud API obtained from {APIKEY_URL}',
|
||||
'auth_secret')
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add: Callable[..., None],
|
||||
|
|
@ -43,50 +43,9 @@ class Authenticator(dns_common.DNSAuthenticator):
|
|||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Sakura Cloud API.'
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'Sakura Cloud credentials file',
|
||||
{
|
||||
'api-token': f'API token for Sakura Cloud API obtained from {APIKEY_URL}',
|
||||
'api-secret': f'API secret for Sakura Cloud API obtained from {APIKEY_URL}',
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_sakuracloud_client().add_txt_record(
|
||||
domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
self._get_sakuracloud_client().del_txt_record(
|
||||
domain, validation_name, validation)
|
||||
|
||||
def _get_sakuracloud_client(self) -> "_SakuraCloudLexiconClient":
|
||||
if not self.credentials: # pragma: no cover
|
||||
raise errors.Error("Plugin has not been prepared.")
|
||||
return _SakuraCloudLexiconClient(
|
||||
cast(str, self.credentials.conf('api-token')),
|
||||
cast(str, self.credentials.conf('api-secret')),
|
||||
self.ttl
|
||||
)
|
||||
|
||||
|
||||
class _SakuraCloudLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the Sakura Cloud via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_token: str, api_secret: str, ttl: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
config = dns_common_lexicon.build_lexicon_config('sakuracloud', {
|
||||
'ttl': ttl,
|
||||
}, {
|
||||
'auth_token': api_token,
|
||||
'auth_secret': api_secret,
|
||||
})
|
||||
|
||||
self.provider = sakuracloud.Provider(config)
|
||||
@property
|
||||
def _provider_name(self) -> str:
|
||||
return 'sakuracloud'
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for certbot_dns_sakuracloud._internal.dns_sakuracloud."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
|
@ -16,8 +14,12 @@ from certbot.tests import util as test_util
|
|||
API_TOKEN = '00000000-0000-0000-0000-000000000000'
|
||||
API_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.')
|
||||
LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
@ -35,24 +37,6 @@ class AuthenticatorTest(test_util.TempDirTestCase,
|
|||
|
||||
self.auth = Authenticator(self.config, "sakuracloud")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_sakuracloud_client | pylint: disable=protected-access
|
||||
self.auth._get_sakuracloud_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class SakuraCloudLexiconClientTest(unittest.TestCase,
|
||||
dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN))
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_sakuracloud._internal.dns_sakuracloud import _SakuraCloudLexiconClient
|
||||
|
||||
self.client = _SakuraCloudLexiconClient(API_TOKEN, API_SECRET, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from setuptools import setup
|
|||
version = '2.7.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'dns-lexicon>=3.2.1',
|
||||
'dns-lexicon>=3.14.1',
|
||||
'setuptools>=41.6.0',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
|||
### Added
|
||||
|
||||
* Add `certbot.util.LooseVersion` class. See [GH #9489](https://github.com/certbot/certbot/issues/9489).
|
||||
* Add a new base class `certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator` to implement a DNS
|
||||
authenticator plugin backed by Lexicon to communicate with the provider DNS API. This approach relies
|
||||
heavily on conventions to reduce the implementation complexity of a new plugin.
|
||||
* Add a new test base class `certbot.plugins.dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest` to
|
||||
help testing DNS plugins implemented on top of `LexiconDNSAuthenticator`.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -14,6 +19,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
|
|||
of global state previously needed to inspect whether a user set an argument or not.
|
||||
* Support for Python 3.7 was deprecated and will be removed in our next planned release.
|
||||
* Added `RENEWED_DOMAINS` and `FAILED_DOMAINS` environment variables for consumption by post renewal hooks.
|
||||
* Deprecates `LexiconClient` base class and `build_lexicon_config` function in
|
||||
`certbot.plugins.dns_common_lexicon` module in favor of `LexiconDNSAuthenticator`.
|
||||
* Deprecates `BaseLexiconAuthenticatorTest` and `BaseLexiconClientTest` test base classes of
|
||||
`certbot.plugins.dns_test_common_lexicon` module in favor of `BaseLexiconDNSAuthenticatorTest`.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
"""Tests for certbot.plugins.dns_common_lexicon."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
from certbot.plugins import dns_test_common_lexicon
|
||||
|
||||
|
||||
class LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
class _FakeLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.client = LexiconClientTest._FakeLexiconClient()
|
||||
self.provider_mock = mock.MagicMock()
|
||||
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||
|
|
@ -1,14 +1,22 @@
|
|||
"""Common code for DNS Authenticator Plugins built on Lexicon."""
|
||||
import abc
|
||||
import logging
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_common
|
||||
|
||||
|
|
@ -18,18 +26,23 @@ from certbot.plugins import dns_common
|
|||
# always importable, even if it does not make sense to use it
|
||||
# if Lexicon is not available, obviously.
|
||||
try:
|
||||
from lexicon.client import Client
|
||||
from lexicon.config import ConfigResolver
|
||||
from lexicon.providers.base import Provider
|
||||
except ImportError:
|
||||
from lexicon.interfaces import Provider
|
||||
except ImportError: # pragma: no cover
|
||||
Client = None
|
||||
ConfigResolver = None
|
||||
Provider = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LexiconClient:
|
||||
class LexiconClient: # pragma: no cover
|
||||
"""
|
||||
Encapsulates all communication with a DNS provider via Lexicon.
|
||||
|
||||
.. deprecated:: 2.7.0
|
||||
Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -122,14 +135,18 @@ class LexiconClient:
|
|||
|
||||
def build_lexicon_config(lexicon_provider_name: str,
|
||||
lexicon_options: Mapping[str, Any], provider_options: Mapping[str, Any]
|
||||
) -> Union[ConfigResolver, Dict[str, Any]]:
|
||||
) -> Union[ConfigResolver, Dict[str, Any]]: # pragma: no cover
|
||||
"""
|
||||
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
|
||||
|
||||
.. deprecated:: 2.7.0
|
||||
Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead.
|
||||
"""
|
||||
config: Union[ConfigResolver, Dict[str, Any]] = {'provider_name': lexicon_provider_name}
|
||||
config.update(lexicon_options)
|
||||
|
|
@ -144,3 +161,143 @@ def build_lexicon_config(lexicon_provider_name: str,
|
|||
config = ConfigResolver().with_dict(config).with_env()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class LexiconDNSAuthenticator(dns_common.DNSAuthenticator):
|
||||
"""
|
||||
Base class for a DNS authenticator that uses Lexicon client
|
||||
as backend to execute DNS record updates
|
||||
"""
|
||||
|
||||
def __init__(self, config: configuration.NamespaceConfig, name: str):
|
||||
super().__init__(config, name)
|
||||
self._provider_options: List[Tuple[str, str, str]] = []
|
||||
self._credentials: dns_common.CredentialsConfiguration
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def _provider_name(self) -> str:
|
||||
"""
|
||||
The name of the Lexicon provider to use
|
||||
"""
|
||||
|
||||
@property
|
||||
def _ttl(self) -> int:
|
||||
"""
|
||||
Time to live to apply to the DNS records created by this Authenticator
|
||||
"""
|
||||
return 60
|
||||
|
||||
def _add_provider_option(self, creds_var_name: str, creds_var_label: str,
|
||||
lexicon_provider_option_name: str) -> None:
|
||||
self._provider_options.append(
|
||||
(creds_var_name, creds_var_label, lexicon_provider_option_name))
|
||||
|
||||
def _build_lexicon_config(self, domain: str) -> ConfigResolver:
|
||||
if not hasattr(self, '_credentials'): # pragma: no cover
|
||||
self._setup_credentials()
|
||||
|
||||
dict_config = {
|
||||
'domain': domain,
|
||||
'provider_name': self._provider_name,
|
||||
'ttl': self._ttl,
|
||||
self._provider_name: {item[2]: self._credentials.conf(item[0])
|
||||
for item in self._provider_options}
|
||||
}
|
||||
return ConfigResolver().with_dict(dict_config).with_env()
|
||||
|
||||
def _setup_credentials(self) -> None:
|
||||
self._credentials = self._configure_credentials(
|
||||
key='credentials',
|
||||
label=f'Credentials INI file for {self._provider_name} DNS authenticator',
|
||||
required_variables={item[0]: item[1] for item in self._provider_options},
|
||||
)
|
||||
|
||||
def _perform(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
resolved_domain = self._resolve_domain(domain)
|
||||
|
||||
try:
|
||||
with Client(self._build_lexicon_config(resolved_domain)) as operations:
|
||||
operations.create_record(rtype='TXT', name=validation_name, content=validation)
|
||||
except RequestException as e:
|
||||
logger.debug('Encountered error adding TXT record: %s', e, exc_info=True)
|
||||
raise errors.PluginError('Error adding TXT record: {0}'.format(e))
|
||||
|
||||
def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:
|
||||
try:
|
||||
resolved_domain = self._resolve_domain(domain)
|
||||
except errors.PluginError as e:
|
||||
logger.debug('Encountered error finding domain_id during deletion: %s', e,
|
||||
exc_info=True)
|
||||
return
|
||||
|
||||
try:
|
||||
with Client(self._build_lexicon_config(resolved_domain)) as operations:
|
||||
operations.delete_record(rtype='TXT', name=validation_name, content=validation)
|
||||
except RequestException as e:
|
||||
logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True)
|
||||
|
||||
def _resolve_domain(self, domain: str) -> str:
|
||||
domain_name_guesses = dns_common.base_domain_name_guesses(domain)
|
||||
|
||||
for domain_name in domain_name_guesses:
|
||||
try:
|
||||
# Using client as a context manager requires `dns-lexicon>=3.14` and we may want to
|
||||
# provide better checks and error handling around this in the future.
|
||||
with Client(self._build_lexicon_config(domain_name)):
|
||||
return domain_name
|
||||
except HTTPError as e:
|
||||
result1 = self._handle_http_error(e, domain_name)
|
||||
|
||||
if result1:
|
||||
raise result1
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
result2 = self._handle_general_error(e, domain_name)
|
||||
|
||||
if result2:
|
||||
raise result2 # pylint: disable=raising-bad-type
|
||||
|
||||
raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}'
|
||||
.format(domain, domain_name_guesses))
|
||||
|
||||
def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:
|
||||
return errors.PluginError('Error determining zone identifier for {0}: {1}.'
|
||||
.format(domain_name, e))
|
||||
|
||||
def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:
|
||||
if not str(e).startswith('No domain found'):
|
||||
return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}'
|
||||
.format(domain_name, e))
|
||||
return None
|
||||
|
||||
|
||||
# This class takes a similar approach to the cryptography project to deprecate attributes
|
||||
# in public modules. See the _ModuleWithDeprecation class here:
|
||||
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
|
||||
class _DeprecationModule:
|
||||
"""
|
||||
Internal class delegating to a module, and displaying warnings when attributes
|
||||
related to deprecated attributes in the current module.
|
||||
"""
|
||||
def __init__(self, module: ModuleType):
|
||||
self.__dict__['_module'] = module
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
if attr in ('LexiconClient', 'build_lexicon_config'):
|
||||
warnings.warn(f'{attr} attribute in {__name__} module is deprecated '
|
||||
'and will be removed soon.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return getattr(self._module, attr)
|
||||
|
||||
def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover
|
||||
setattr(self._module, attr, value)
|
||||
|
||||
def __delattr__(self, attr: str) -> Any: # pragma: no cover
|
||||
delattr(self._module, attr)
|
||||
|
||||
def __dir__(self) -> List[str]: # pragma: no cover
|
||||
return ['_module'] + dir(self._module)
|
||||
|
||||
|
||||
# Patching ourselves to warn about deprecation and planned removal of some elements in the module.
|
||||
sys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__]))
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ class _AuthenticatorCallableTestCase(Protocol):
|
|||
https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual
|
||||
"""
|
||||
|
||||
def assertRaises(self, *unused_args: Any) -> None:
|
||||
"""
|
||||
See
|
||||
https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises
|
||||
"""
|
||||
|
||||
|
||||
class BaseAuthenticatorTest:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
"""Base test class for DNS authenticators built on Lexicon."""
|
||||
import contextlib
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
import warnings
|
||||
|
||||
import josepy as jose
|
||||
from requests.exceptions import HTTPError
|
||||
|
|
@ -11,11 +19,15 @@ from requests.exceptions import RequestException
|
|||
from certbot import errors
|
||||
from certbot.achallenges import AnnotatedChallenge
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins.dns_common_lexicon import LexiconClient
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
from certbot.plugins.dns_common_lexicon import LexiconClient
|
||||
|
||||
from certbot.plugins.dns_test_common import _AuthenticatorCallableTestCase
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
Protocol = object
|
||||
|
|
@ -23,6 +35,11 @@ else:
|
|||
DOMAIN = 'example.com'
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('No domain found')
|
||||
GENERIC_ERROR = RequestException
|
||||
LOGIN_ERROR = HTTPError('400 Client Error: ...')
|
||||
UNKNOWN_LOGIN_ERROR = HTTPError('500 Surprise! Error: ...')
|
||||
|
||||
|
||||
class _AuthenticatorCallableLexiconTestCase(_AuthenticatorCallableTestCase, Protocol):
|
||||
"""
|
||||
|
|
@ -59,7 +76,7 @@ class _LexiconAwareTestCase(Protocol):
|
|||
# These classes are intended to be subclassed/mixed in, so not all members are defined.
|
||||
# pylint: disable=no-member
|
||||
|
||||
class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):
|
||||
class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest): # pragma: no cover
|
||||
|
||||
@test_util.patch_display_util()
|
||||
def test_perform(self: _AuthenticatorCallableLexiconTestCase,
|
||||
|
|
@ -77,11 +94,11 @@ class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):
|
|||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
|
||||
class BaseLexiconClientTest:
|
||||
DOMAIN_NOT_FOUND = Exception('No domain found')
|
||||
GENERIC_ERROR = RequestException
|
||||
LOGIN_ERROR = HTTPError('400 Client Error: ...')
|
||||
UNKNOWN_LOGIN_ERROR = HTTPError('500 Surprise! Error: ...')
|
||||
class BaseLexiconClientTest: # pragma: no cover
|
||||
DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND
|
||||
GENERIC_ERROR = GENERIC_ERROR
|
||||
LOGIN_ERROR = LOGIN_ERROR
|
||||
UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR
|
||||
|
||||
record_prefix = "_acme-challenge"
|
||||
record_name = record_prefix + "." + DOMAIN
|
||||
|
|
@ -175,3 +192,169 @@ class BaseLexiconClientTest:
|
|||
self.provider_mock.delete_record.side_effect = self.GENERIC_ERROR
|
||||
|
||||
self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
|
||||
class _BaseLexiconDNSAuthenticatorTestProto(_AuthenticatorCallableTestCase, Protocol):
|
||||
"""Protocol for BaseLexiconDNSAuthenticatorTest instances"""
|
||||
DOMAIN_NOT_FOUND: Exception
|
||||
GENERIC_ERROR: Exception
|
||||
LOGIN_ERROR: Exception
|
||||
UNKNOWN_LOGIN_ERROR: Exception
|
||||
|
||||
achall: AnnotatedChallenge
|
||||
|
||||
|
||||
class BaseLexiconDNSAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):
|
||||
|
||||
DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND
|
||||
GENERIC_ERROR = GENERIC_ERROR
|
||||
LOGIN_ERROR = LOGIN_ERROR
|
||||
UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR
|
||||
|
||||
def test_perform_succeed(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, mock_operations):
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
mock_client.assert_called()
|
||||
config = mock_client.call_args[0][0]
|
||||
self.assertEqual(DOMAIN, config.resolve('lexicon:domain'))
|
||||
|
||||
mock_operations.create_record.assert_called_with(
|
||||
rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY)
|
||||
|
||||
def test_perform_with_one_domain_resolution_failure_succeed(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, mock_operations):
|
||||
mock_client.return_value.__enter__.side_effect = [
|
||||
self.DOMAIN_NOT_FOUND, # First resolution domain attempt
|
||||
mock_operations, # Second resolution domain attempt
|
||||
mock_operations, # Create record operation
|
||||
]
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
def test_perform_with_two_domain_resolution_failures_raise(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.auth.perform,
|
||||
[self.achall])
|
||||
|
||||
def test_perform_with_domain_resolution_general_failure_raise(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.auth.perform,
|
||||
[self.achall])
|
||||
|
||||
def test_perform_with_auth_failure_raise(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.side_effect = self.LOGIN_ERROR
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.auth.perform,
|
||||
[self.achall])
|
||||
|
||||
def test_perform_with_unknown_auth_failure_raise(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.side_effect = self.UNKNOWN_LOGIN_ERROR
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.auth.perform,
|
||||
[self.achall])
|
||||
|
||||
def test_perform_with_create_record_failure_raise(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with test_util.patch_display_util():
|
||||
with _patch_lexicon_client() as (_, mock_operations):
|
||||
mock_operations.create_record.side_effect = self.GENERIC_ERROR
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.auth.perform,
|
||||
[self.achall])
|
||||
|
||||
def test_cleanup_success(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
self.auth._attempt_cleanup = True # _attempt_cleanup | pylint: disable=protected-access
|
||||
with _patch_lexicon_client() as (mock_client, mock_operations):
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
mock_client.assert_called()
|
||||
config = mock_client.call_args[0][0]
|
||||
self.assertEqual(DOMAIN, config.resolve('lexicon:domain'))
|
||||
|
||||
mock_operations.delete_record.assert_called_with(
|
||||
rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY)
|
||||
|
||||
def test_cleanup_with_auth_failure_ignore(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.side_effect = self.LOGIN_ERROR
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
def test_cleanup_with_unknown_auth_failure_ignore(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.side_effect = self.LOGIN_ERROR
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
def test_cleanup_with_domain_resolution_failure_ignore(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
def test_cleanup_with_domain_resolution_general_failure_ignore(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with _patch_lexicon_client() as (mock_client, _):
|
||||
mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
def test_cleanup_with_delete_record_failure_ignore(
|
||||
self: _BaseLexiconDNSAuthenticatorTestProto) -> None:
|
||||
with _patch_lexicon_client() as (_, mock_operations):
|
||||
mock_operations.create_record.side_effect = self.GENERIC_ERROR
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_lexicon_client() -> Generator[Tuple[MagicMock, MagicMock], None, None]:
|
||||
with mock.patch('certbot.plugins.dns_common_lexicon.Client') as mock_client:
|
||||
mock_operations = MagicMock()
|
||||
mock_client.return_value.__enter__.return_value = mock_operations
|
||||
yield mock_client, mock_operations
|
||||
|
||||
|
||||
# This class takes a similar approach to the cryptography project to deprecate attributes
|
||||
# in public modules. See the _ModuleWithDeprecation class here:
|
||||
# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129
|
||||
class _DeprecationModule:
|
||||
"""
|
||||
Internal class delegating to a module, and displaying warnings when attributes
|
||||
related to deprecated attributes in the current module.
|
||||
"""
|
||||
def __init__(self, module: ModuleType):
|
||||
self.__dict__['_module'] = module
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
if attr in ('BaseLexiconAuthenticatorTest', 'BaseLexiconClientTest'):
|
||||
warnings.warn(f'{attr} attribute in {__name__} module is deprecated '
|
||||
'and will be removed soon.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return getattr(self._module, attr)
|
||||
|
||||
def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover
|
||||
setattr(self._module, attr, value)
|
||||
|
||||
def __delattr__(self, attr: str) -> Any: # pragma: no cover
|
||||
delattr(self._module, attr)
|
||||
|
||||
def __dir__(self) -> List[str]: # pragma: no cover
|
||||
return ['_module'] + dir(self._module)
|
||||
|
||||
|
||||
# Patching ourselves to warn about deprecation and planned removal of some elements in the module.
|
||||
sys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__]))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ apacheconfig==0.3.2 ; python_version >= "3.7" and python_version < "3.8"
|
|||
appdirs==1.4.4 ; python_version >= "3.7" and python_version < "3.8"
|
||||
asn1crypto==0.24.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
astroid==2.15.6 ; python_full_version >= "3.7.2" and python_version < "3.8"
|
||||
beautifulsoup4==4.12.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
boto3==1.15.15 ; python_version >= "3.7" and python_version < "3.8"
|
||||
botocore==1.18.15 ; python_version >= "3.7" and python_version < "3.8"
|
||||
cachetools==5.3.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
|
@ -11,7 +12,7 @@ certifi==2023.7.22 ; python_version >= "3.7" and python_version < "3.8"
|
|||
cffi==1.11.5 ; python_version >= "3.7" and python_version < "3.8"
|
||||
chardet==3.0.4 ; python_version >= "3.7" and python_version < "3.8"
|
||||
cloudflare==1.5.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "3.8" and sys_platform == "win32"
|
||||
colorama==0.4.6 ; python_version < "3.8" and sys_platform == "win32" and python_version >= "3.7"
|
||||
configargparse==1.5.3 ; python_version >= "3.7" and python_version < "3.8"
|
||||
configobj==5.0.6 ; python_version >= "3.7" and python_version < "3.8"
|
||||
coverage==7.2.7 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
|
@ -20,7 +21,7 @@ cython==0.29.36 ; python_version >= "3.7" and python_version < "3.8"
|
|||
dill==0.3.7 ; python_full_version >= "3.7.2" and python_version < "3.8"
|
||||
distlib==0.3.7 ; python_version >= "3.7" and python_version < "3.8"
|
||||
distro==1.0.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
dns-lexicon==3.2.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
dns-lexicon==3.14.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
dnspython==1.15.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
exceptiongroup==1.1.3 ; python_version >= "3.7" and python_version < "3.8"
|
||||
execnet==2.0.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
|
@ -75,11 +76,12 @@ rsa==4.9 ; python_version >= "3.7" and python_version < "3.8"
|
|||
s3transfer==0.3.7 ; python_version >= "3.7" and python_version < "3.8"
|
||||
setuptools==41.6.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
six==1.11.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
soupsieve==2.4.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
tldextract==3.5.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
tomli==2.0.1 ; python_version < "3.8" and python_version >= "3.7"
|
||||
tomlkit==0.12.1 ; python_full_version >= "3.7.2" and python_version < "3.8"
|
||||
tox==1.9.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
typed-ast==1.5.5 ; python_version >= "3.7" and python_version < "3.8"
|
||||
typed-ast==1.5.5 ; python_version < "3.8" and python_version >= "3.7"
|
||||
types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
types-httplib2==0.22.0.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
types-pyopenssl==23.0.0.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
|
@ -91,7 +93,7 @@ types-requests==2.31.0.2 ; python_version >= "3.7" and python_version < "3.8"
|
|||
types-setuptools==68.2.0.0 ; python_version >= "3.7" and python_version < "3.8"
|
||||
types-six==1.16.21.9 ; python_version >= "3.7" and python_version < "3.8"
|
||||
types-urllib3==1.26.25.14 ; python_version >= "3.7" and python_version < "3.8"
|
||||
typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
typing-extensions==4.7.1 ; python_version < "3.8" and python_version >= "3.7"
|
||||
uritemplate==3.0.1 ; python_version >= "3.7" and python_version < "3.8"
|
||||
urllib3==1.24.2 ; python_version >= "3.7" and python_version < "3.8"
|
||||
virtualenv==20.4.7 ; python_version >= "3.7" and python_version < "3.8"
|
||||
|
|
|
|||
|
|
@ -75,11 +75,6 @@ poetry = "<1.3.0"
|
|||
# https://github.com/certbot/certbot/issues/9606.
|
||||
setuptools = "<67.5.0"
|
||||
|
||||
# Lexicon 3.14+ deprecates several private APIs and create stubs packages, leading
|
||||
# to several warnings and lint/mypy errors. Let's pin it to 3.13 until this is fixed
|
||||
# with https://github.com/certbot/certbot/pull/9746.
|
||||
dns-lexicon = "<3.14"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ cloudflare = "1.5.1"
|
|||
configobj = "5.0.6"
|
||||
cryptography = "3.2.1"
|
||||
distro = "1.0.1"
|
||||
dns-lexicon = "3.2.1"
|
||||
dns-lexicon = "3.14.1"
|
||||
dnspython = "1.15.0"
|
||||
funcsigs = "0.4"
|
||||
google-api-python-client = "1.6.5"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ backports-cached-property==1.0.2 ; python_version >= "3.7" and python_version <
|
|||
bcrypt==4.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
beautifulsoup4==4.12.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bleach==6.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
boto3==1.28.43 ; python_version >= "3.7" and python_version < "4.0"
|
||||
botocore==1.31.43 ; python_version >= "3.7" and python_version < "4.0"
|
||||
boto3==1.28.45 ; python_version >= "3.7" and python_version < "4.0"
|
||||
botocore==1.31.45 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachecontrol==0.12.14 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachetools==5.3.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cachy==0.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
@ -28,7 +28,7 @@ cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
|||
charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cleo==1.0.0a5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cloudflare==2.11.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
|
||||
colorama==0.4.6 ; python_version < "4.0" and sys_platform == "win32" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows"
|
||||
configargparse==1.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
configobj==5.0.8 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coverage==7.2.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
@ -40,18 +40,18 @@ deprecated==1.2.14 ; python_version >= "3.7" and python_version < "4.0"
|
|||
dill==0.3.7 ; python_full_version >= "3.7.2" and python_version < "4.0"
|
||||
distlib==0.3.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
distro==1.8.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
dns-lexicon==3.13.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
dns-lexicon==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
dnspython==2.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
docutils==0.19 ; python_version >= "3.7" and python_version < "4.0"
|
||||
docutils==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
dulwich==0.20.50 ; python_version >= "3.7" and python_version < "4.0"
|
||||
exceptiongroup==1.1.3 ; python_version >= "3.7" and python_version < "3.11"
|
||||
execnet==2.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
fabric==3.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
filelock==3.12.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-core==2.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-python-client==2.98.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth-httplib2==0.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth==2.22.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-api-python-client==2.99.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth-httplib2==0.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
google-auth==2.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
googleapis-common-protos==1.60.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
html5lib==1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httplib2==0.22.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
@ -97,7 +97,7 @@ pickleshare==0.7.5 ; python_version >= "3.7" and python_version < "4.0"
|
|||
pip==23.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pkginfo==1.9.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pkgutil-resolve-name==1.3.10 ; python_version >= "3.7" and python_version < "3.9"
|
||||
platformdirs==2.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
platformdirs==2.6.2 ; python_version < "4.0" and python_version >= "3.7"
|
||||
pluggy==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ply==3.11 ; python_version >= "3.7" and python_version < "4.0"
|
||||
poetry-core==1.3.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
@ -147,21 +147,22 @@ shellingham==1.5.3 ; python_version >= "3.7" and python_version < "4.0"
|
|||
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
snowballstemmer==2.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
soupsieve==2.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinx-rtd-theme==0.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinx-rtd-theme==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinx==5.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-applehelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-htmlhelp==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-jquery==4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tldextract==3.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_full_version <= "3.11.0a6"
|
||||
tomlkit==0.12.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomlkit==0.12.1 ; python_version < "4.0" and python_version >= "3.7"
|
||||
tox==3.28.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
traitlets==5.9.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
twine==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typed-ast==1.5.5 ; python_version >= "3.7" and python_version < "3.8"
|
||||
typed-ast==1.5.5 ; python_version < "3.8" and python_version >= "3.7"
|
||||
types-httplib2==0.22.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pyopenssl==23.2.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
types-pyrfc3339==1.1.1.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
@ -179,7 +180,7 @@ virtualenv==20.21.1 ; python_version >= "3.7" and python_version < "4.0"
|
|||
wcwidth==0.2.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
webencodings==0.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
wheel==0.41.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
wrapt==1.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
wrapt==1.15.0 ; python_version < "4.0" and python_version >= "3.7"
|
||||
xattr==0.9.9 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin"
|
||||
yarg==0.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
zipp==3.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue