initial commit

This commit is contained in:
Robert Kaussow 2020-04-19 17:57:48 +02:00
commit a5a7cb9f9b
14 changed files with 1130 additions and 0 deletions

212
.drone.jsonnet Normal file
View File

@ -0,0 +1,212 @@
local PythonVersion(pyversion='2.7') = {
name: 'python' + std.strReplace(pyversion, '.', ''),
image: 'python:' + pyversion,
environment: {
PY_COLORS: 1,
},
commands: [
'pip install -r dev-requirements.txt -qq',
'pip install -qq .',
'pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail',
],
depends_on: [
'clone',
],
};
local PipelineLint = {
kind: 'pipeline',
name: 'lint',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'flake8',
image: 'python:3.8',
environment: {
PY_COLORS: 1,
},
commands: [
'pip install -r dev-requirements.txt -qq',
'pip install -qq .',
'flake8 ./certbot_dns_corenetworks',
],
},
],
trigger: {
ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineTest = {
kind: 'pipeline',
name: 'test',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
PythonVersion(pyversion='2.7'),
PythonVersion(pyversion='3.5'),
PythonVersion(pyversion='3.6'),
PythonVersion(pyversion='3.7'),
PythonVersion(pyversion='3.8'),
{
name: 'codecov',
image: 'python:3.8',
environment: {
PY_COLORS: 1,
CODECOV_TOKEN: { from_secret: 'codecov_token' },
},
commands: [
'pip install codecov -qq',
'codecov --required -X gcov',
],
depends_on: [
'python27',
'python35',
'python36',
'python37',
'python38',
],
},
],
depends_on: [
'lint',
],
trigger: {
ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineSecurity = {
kind: 'pipeline',
name: 'security',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'bandit',
image: 'python:3.8',
environment: {
PY_COLORS: 1,
},
commands: [
'pip install -r dev-requirements.txt -qq',
'pip install -qq .',
'bandit -r ./certbot_dns_corenetworks -x ./certbot_dns_corenetworks/test',
],
},
],
depends_on: [
'test',
],
trigger: {
ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuildPackage = {
kind: 'pipeline',
name: 'build-package',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'build',
image: 'python:3.8',
environment: {
SETUPTOOLS_SCM_PRETEND_VERSION: '${DRONE_TAG##v}',
},
commands: [
'python setup.py sdist bdist_wheel',
],
},
{
name: 'checksum',
image: 'alpine',
commands: [
'cd dist/ && sha256sum * > ../sha256sum.txt',
],
},
{
name: 'publish-github',
image: 'plugins/github-release',
settings: {
overwrite: true,
api_key: { from_secret: 'github_token' },
files: ['dist/*', 'sha256sum.txt'],
title: '${DRONE_TAG}',
note: 'CHANGELOG.md',
},
when: {
ref: ['refs/tags/**'],
},
},
{
name: 'publish-pypi',
image: 'plugins/pypi',
settings: {
username: { from_secret: 'pypi_username' },
password: { from_secret: 'pypi_password' },
repository: 'https://upload.pypi.org/legacy/',
skip_build: true,
},
when: {
ref: ['refs/tags/**'],
},
},
],
depends_on: [
'security',
],
trigger: {
ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineNotifications = {
kind: 'pipeline',
name: 'notifications',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'matrix',
image: 'plugins/matrix',
settings: {
homeserver: { from_secret: 'matrix_homeserver' },
roomid: { from_secret: 'matrix_roomid' },
template: 'Status: **{{ build.status }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}<br/> Message: {{ build.message }}',
username: { from_secret: 'matrix_username' },
password: { from_secret: 'matrix_password' },
},
when: {
status: ['success', 'failure'],
},
},
],
depends_on: [
'build-package',
],
trigger: {
ref: ['refs/heads/master', 'refs/tags/**'],
status: ['success', 'failure'],
},
};
[
PipelineLint,
PipelineTest,
PipelineSecurity,
PipelineBuildPackage,
PipelineNotifications,
]

240
.drone.yml Normal file
View File

@ -0,0 +1,240 @@
---
kind: pipeline
name: lint
platform:
os: linux
arch: amd64
steps:
- name: flake8
image: python:3.8
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- flake8 ./certbot_dns_corenetworks
environment:
PY_COLORS: 1
trigger:
ref:
- refs/heads/master
- refs/tags/**
- refs/pull/**
---
kind: pipeline
name: test
platform:
os: linux
arch: amd64
steps:
- name: python27
image: python:2.7
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail
environment:
PY_COLORS: 1
depends_on:
- clone
- name: python35
image: python:3.5
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail
environment:
PY_COLORS: 1
depends_on:
- clone
- name: python36
image: python:3.6
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail
environment:
PY_COLORS: 1
depends_on:
- clone
- name: python37
image: python:3.7
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail
environment:
PY_COLORS: 1
depends_on:
- clone
- name: python38
image: python:3.8
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail
environment:
PY_COLORS: 1
depends_on:
- clone
- name: codecov
image: python:3.8
commands:
- pip install codecov -qq
- codecov --required -X gcov
environment:
CODECOV_TOKEN:
from_secret: codecov_token
PY_COLORS: 1
depends_on:
- python27
- python35
- python36
- python37
- python38
trigger:
ref:
- refs/heads/master
- refs/tags/**
- refs/pull/**
depends_on:
- lint
---
kind: pipeline
name: security
platform:
os: linux
arch: amd64
steps:
- name: bandit
image: python:3.8
commands:
- pip install -r dev-requirements.txt -qq
- pip install -qq .
- bandit -r ./certbot_dns_corenetworks -x ./certbot_dns_corenetworks/test
environment:
PY_COLORS: 1
trigger:
ref:
- refs/heads/master
- refs/tags/**
- refs/pull/**
depends_on:
- test
---
kind: pipeline
name: build-package
platform:
os: linux
arch: amd64
steps:
- name: build
image: python:3.8
commands:
- python setup.py sdist bdist_wheel
environment:
SETUPTOOLS_SCM_PRETEND_VERSION: ${DRONE_TAG##v}
- name: checksum
image: alpine
commands:
- cd dist/ && sha256sum * > ../sha256sum.txt
- name: publish-github
image: plugins/github-release
settings:
api_key:
from_secret: github_token
files:
- dist/*
- sha256sum.txt
note: CHANGELOG.md
overwrite: true
title: ${DRONE_TAG}
when:
ref:
- refs/tags/**
- name: publish-pypi
image: plugins/pypi
settings:
password:
from_secret: pypi_password
repository: https://upload.pypi.org/legacy/
skip_build: true
username:
from_secret: pypi_username
when:
ref:
- refs/tags/**
trigger:
ref:
- refs/heads/master
- refs/tags/**
- refs/pull/**
depends_on:
- security
---
kind: pipeline
name: notifications
platform:
os: linux
arch: amd64
steps:
- name: matrix
image: plugins/matrix
settings:
homeserver:
from_secret: matrix_homeserver
password:
from_secret: matrix_password
roomid:
from_secret: matrix_roomid
template: "Status: **{{ build.status }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}<br/> Message: {{ build.message }}"
username:
from_secret: matrix_username
when:
status:
- success
- failure
trigger:
ref:
- refs/heads/master
- refs/tags/**
status:
- success
- failure
depends_on:
- build-package
---
kind: signature
hmac: 09f1f5c2f46dd90b159b5c5de705afce849ea7fb4772b96aa9827a6d6bb97e3e
...

19
.flake8 Normal file
View File

@ -0,0 +1,19 @@
[flake8]
ignore = D103, D107, W503
max-line-length = 99
inline-quotes = double
exclude =
.git
.tox
__pycache__
build
dist
test
tests
*.pyc
*.egg-info
.cache
.eggs
env*
application-import-names = corenetworks
format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s

60
.github/settings.yml vendored Normal file
View File

@ -0,0 +1,60 @@
---
repository:
name: corenetworks
description: Python library for the core-networks.de DNS API
homepage: https://corenetworks.geekdocs.de
topics: corenetworks, api, dns, python
private: false
has_issues: true
has_projects: false
has_wiki: false
has_downloads: false
default_branch: master
allow_squash_merge: true
allow_merge_commit: true
allow_rebase_merge: true
labels:
- name: bug
color: d73a4a
description: Something isn't working
- name: documentation
color: 0075ca
description: Improvements or additions to documentation
- name: duplicate
color: cfd3d7
description: This issue or pull request already exists
- name: enhancement
color: a2eeef
description: New feature or request
- name: good first issue
color: 7057ff
description: Good for newcomers
- name: help wanted
color: 008672
description: Extra attention is needed
- name: invalid
color: e4e669
description: This doesn't seem right
- name: question
color: d876e3
description: Further information is requested
- name: wontfix
color: ffffff
description: This will not be worked on
branches:
- name: master
protection:
required_pull_request_reviews: null
required_status_checks:
strict: true
contexts:
- continuous-integration/drone/pr
enforce_admins: null
restrictions: null
...

113
.gitignore vendored Normal file
View File

@ -0,0 +1,113 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env*
# virtualenv
.venv
venv/
ENV/
env/
env*/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# Ignore ide addons
.server-script
.on-save.json
.vscode
.pytest_cache
pip-wheel-metadata
# Hugo documentation
docs/themes/
docs/public/
resources/_gen/
# Misc
.local/
.corenetworks*
docs/content/api/corenetworks/

1
CHANGELOG.md Normal file
View File

@ -0,0 +1 @@
* initial release

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Robert Kaussow <mail@geeklabor.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# certbot-dns-corenetworks
[![Build Status](https://img.shields.io/drone/build/xoxys/certbot-dns-corenetworks?logo=drone)](https://cloud.drone.io/xoxys/certbot-dns-corenetworks)
[![Python Version](https://img.shields.io/pypi/pyversions/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/)
[![PyPi Status](https://img.shields.io/pypi/status/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/)
[![PyPi Release](https://img.shields.io/pypi/v/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/)
[![License: MIT](https://img.shields.io/github/license/xoxys/certbot-dns-corenetworks)](LICENSE)
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
## Maintainers and Contributors
[Robert Kaussow](https://github.com/xoxys)

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""Default package."""
__author__ = "Robert Kaussow"
__project__ = "certbot_dns_corenetworks"
__license__ = "MIT"
__maintainer__ = "Robert Kaussow"
__email__ = "mail@geeklabor.de"
__url__ = "https://github.com/xoxys/certbot-dns-corenetworks"
__version__ = "0.1.0"

View File

@ -0,0 +1,228 @@
"""DNS Authenticator for Core Networks."""
import logging
import re
import zope.interface # noqa
from certbot import errors
from certbot import interfaces
from certbot.plugins import dns_common
from corenetworks import CoreNetworks
from corenetworks.exceptions import AuthError
from corenetworks.exceptions import CoreNetworksException
logger = logging.getLogger(__name__)
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Core Networks DNS API."""
description = (
"Obtain certificates using a DNS TXT record "
"(if you are using Core Networks for your domains)."
)
ttl = 300
clientCache = {} # noqa
nameCache = {} # noqa
def __init__(self, *args, **kwargs):
"""Initialize an Core Networks Authenticator."""
super(Authenticator, self).__init__(*args, **kwargs)
self.credentials = None
@classmethod
def add_parser_arguments(cls, add): # noqa
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)
add(
"credentials",
help=("Path to Core Networks account credentials INI file"),
default="/etc/letsencrypt/corenetworks.cfg"
)
def more_info(self): # noqa
return "This plugin configures a DNS TXT record to respond to a dns-01 challenge using " \
"the Core Networks DNS API."
def _setup_credentials(self):
self.credentials = self._configure_credentials(
"credentials", "path to Core Networks API credentials INI file", {
"username": "Username of the Core Networks API account.",
"password": "Password of the Core Networks API account.",
}
)
def _follow_cnames(self, domain, validation_name):
"""
Perform recursive CNAME lookups.
In case there exist a CNAME for the given validation name a recursive CNAME lookup
will be performed automatically. If the optional dependency dnspython is not installed,
the given name is simply returned.
"""
try:
import dns.exception # noqa
import dns.resolver # noqa
import dns.name # noqa
except ImportError:
return validation_name
resolver = dns.resolver.Resolver()
name = dns.name.from_text(validation_name)
while 1:
try:
answer = resolver.query(name, "CNAME")
if 1 <= len(answer):
name = answer[0].target
else:
break
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
break
except (dns.exception.Timeout, dns.resolver.YXDOMAIN, dns.resolver.NoNameservers):
raise errors.PluginError(
"Failed to lookup CNAMEs on your requested domain {0}".format(domain)
)
return name.to_text(True)
def _perform(self, domain, validation_name, validation):
if validation_name in Authenticator.nameCache:
resolved = Authenticator.nameCache[validation_name]
else:
resolved = self._follow_cnames(domain, validation_name)
Authenticator.nameCache[validation_name] = resolved
if resolved != validation_name:
logger.info("Validation record for %s redirected by CNAME(s) to %s", domain, resolved)
self._get_corenetworks_client().add_txt_record(domain, resolved, validation, self.ttl)
def _cleanup(self, domain, validation_name, validation):
resolved = Authenticator.nameCache[validation_name]
self._get_corenetworks_client().del_txt_record(domain, resolved, validation)
def _get_corenetworks_client(self):
key = self.conf("credentials")
if key in Authenticator.clientCache:
client = Authenticator.clientCache[key]
else:
client = _CorenetworksClient(
self.credentials.conf("username"), self.credentials.conf("password")
)
Authenticator.clientCache[key] = client
return client
class _CorenetworksClient(object):
"""Encapsulates all communication with the Core Networks API."""
def __init__(self, user, password, auto_commit=True):
try:
self.client = CoreNetworks(user, password, auto_commit=auto_commit)
except AuthError as e:
raise errors.PluginError("Login failed: {0}".format(str(e)))
def add_txt_record(self, domain_name, record_name, record_content, record_ttl):
"""
Add a TXT record using the supplied information.
Args:
domain_name (str): The requested domain for validation.
record_name (str): The record name (typically beginning with "_acme-challenge.").
record_content (str): The record content (typically the challenge validation).
record_ttl(int): The record TTL (number of seconds that the record may be cached).
Raises:
certbot.errors.PluginError: If an error occurs communicating with the DNS server
"""
zone = self._find_zone(record_name)
name = re.sub(r"\.{}$".format(zone), "", record_name)
try:
self.client.add_record(
zone, {
"name": name,
"type": "TXT",
"data": record_content,
"ttl": record_ttl
}
)
except CoreNetworksException:
raise errors.PluginError(
"Failed to add TXT DNS record {record} to {zone} for {domain}".format(
record=record_name, zone=zone, domain=domain_name
)
)
def del_txt_record(self, domain_name, record_name, record_content):
"""
Delete a TXT record using the supplied information.
Args:
domain_name (str): The requested domain for validation.
record_name (str): The record name (typically beginning with "_acme-challenge.").
record_content (str): The record content (typically the challenge validation).
Returns:
certbot.errors.PluginError: if an error occurs communicating with the DNS server
"""
zone = self._find_zone(record_name)
name = re.sub(r"\.{}$".format(zone), "", record_name)
try:
info = self.client.records(zone, {"name": name, "type": "TXT", "data": record_content})
if (len(info) != 1 or info[0]["name"] != name):
raise NameError("Unknown record")
except NameError as e:
raise errors.PluginError(
"Record {record} not found: {err}".format(record=record_name, err=e)
)
except CoreNetworksException as e:
raise errors.PluginError(
"Could not lookup record {record}: {err}".format(record=record_name, err=e)
)
try:
self.client.delete_record(zone, {"name": name, "type": "TXT", "data": record_content})
except CoreNetworksException:
raise errors.PluginError(
"Failed to delete TXT DNS record {record} of {zone} for {domain}".format(
record=record_name, zone=zone, domain=domain_name
)
)
def _find_zone(self, domain_name):
"""
Find the base domain name for a given domain name.
:param str domain_name: The domain name for which to find the corresponding base domain.
:returns: The base domain name, if found.
:rtype: str
:raises certbot.errors.PluginError: if no matching domain is found.
"""
domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)
for guess in domain_name_guesses:
logger.debug("Testing {0} for domain {1}...".format(guess, domain_name))
try:
info = self.client.zone(guess)[0]
except Exception:
continue
logger.debug("Found zone '{zone}': {info}".format(zone=guess, info=info))
if not info.get("active"):
raise errors.PluginError("Zone {0} is not active".format(guess))
if info.get("type") != "master":
raise errors.PluginError("Zone {0} is not a master zone".format(guess))
return guess
raise errors.PluginError(
"Unable to determine base domain for {0} using names: {1}".format(
domain_name, domain_name_guesses
)
)

19
dev-requirements.txt Normal file
View File

@ -0,0 +1,19 @@
pydocstyle
flake8
flake8-colors
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-isort
flake8-logging-format
flake8-polyfill
flake8-quotes
flake8-pep3101
flake8-eradicate; python_version >= "3.6"
pep8-naming
wheel
pytest
pytest-mock
pytest-cov
bandit
yapf

30
setup.cfg Normal file
View File

@ -0,0 +1,30 @@
[metadata]
description-file = README.md
license_file = LICENSE
[bdist_wheel]
universal = 1
[isort]
default_section = THIRDPARTY
known_first_party = certbot_dns_corenetworks
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
force_single_line = true
line_length = 99
skip_glob = **/.env*,**/env/*,**/docs/*
[yapf]
based_on_style = google
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true
[tool:pytest]
filterwarnings =
ignore::FutureWarning
ignore:.*collections.*:DeprecationWarning
ignore:.*pep8.*:FutureWarning
[coverage:run]
omit = **/test/*

78
setup.py Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Setup script for the package."""
import io
import os
import re
from setuptools import find_packages
from setuptools import setup
PACKAGE_NAME = "certbot_dns_corenetworks"
def get_property(prop, project):
current_dir = os.path.dirname(os.path.realpath(__file__))
result = re.search(
r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop),
open(os.path.join(current_dir, project, "__init__.py")).read(),
)
return result.group(1)
def get_readme(filename="README.md"):
this = os.path.abspath(os.path.dirname(__file__))
with io.open(os.path.join(this, filename), encoding="utf-8") as f:
long_description = f.read()
return long_description
setup(
name=get_property("__project__", PACKAGE_NAME),
description="Core Networks DNS Authenticator plugin for Certbot",
keywords="dns, certbot, automation, corenetworks",
version=get_property("__version__", PACKAGE_NAME),
author=get_property("__author__", PACKAGE_NAME),
author_email=get_property("__email__", PACKAGE_NAME),
url=get_property("__url__", PACKAGE_NAME),
license=get_property("__license__", PACKAGE_NAME),
long_description=get_readme(),
long_description_content_type="text/markdown",
packages=find_packages(exclude=["*.test", "test", "test.*"]),
include_package_data=True,
zip_safe=False,
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4",
entry_points={
"certbot.plugins": [
"dns-corenetworks = certbot_dns_corenetworks.dns_corenetworks:Authenticator"
],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
"Intended Audience :: System Administrators",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: POSIX",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Security",
"Topic :: System :: Networking",
"Topic :: Utilities",
],
install_requires=[
"acme",
"certbot>=0.15",
"setuptools",
"zope.interface",
"corenetworks",
],
dependency_links=[],
)

View File

@ -0,0 +1,84 @@
"""Tests for certbot_dns_ispconfig.dns_ispconfig."""
import unittest
import mock
from certbot import errors
from certbot.compat import os
from certbot.plugins import dns_test_common
from certbot.plugins.dns_test_common import DOMAIN
from certbot.tests import util as test_util
API_USER = "my_user"
API_PASSWORD = "secure"
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
"""Test for Hetzner DNS Authenticator."""
def setUp(self):
from certbot_dns_corenetworks.dns_corenetworks import Authenticator
super(AuthenticatorTest, self).setUp()
path = os.path.join(self.tempdir, "file.ini")
dns_test_common.write({
"corenetworks_username": API_USER,
"corenetworks_password": API_PASSWORD
}, path)
self.config = mock.MagicMock(
corenetworks_credentials=path, corenetworks_propagation_seconds=0
) # don't wait during tests
self.auth = Authenticator(self.config, "corenetworks")
self.mock_client = mock.MagicMock()
# _get_corenetworks_client | pylint: disable=protected-access
self.auth._get_corenetworks_client = mock.MagicMock(return_value=self.mock_client)
def test_perform(self):
self.auth.perform([self.achall])
expected = [
mock.call.add_txt_record(DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY, mock.ANY)
]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_cleanup(self):
# _attempt_cleanup | pylint: disable=protected-access
self.auth.nameCache["_acme-challenge." + DOMAIN] = "_acme-challenge." + DOMAIN
self.auth._attempt_cleanup = True
self.auth.cleanup([self.achall])
expected = [mock.call.del_txt_record(DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_creds(self):
dns_test_common.write({
"corenetworks_username": API_USER,
"corenetworks_password": API_PASSWORD
}, self.config.corenetworks_credentials)
self.auth.perform([self.achall])
expected = [
mock.call.add_txt_record(DOMAIN, "_acme-challenge." + DOMAIN, mock.ANY, mock.ANY)
]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_no_creds(self):
dns_test_common.write({}, self.config.corenetworks_credentials)
self.assertRaises(errors.PluginError, self.auth.perform, [self.achall])
def test_missing_user_or_password(self):
dns_test_common.write({"corenetworks_username": API_USER},
self.config.corenetworks_credentials)
self.assertRaises(errors.PluginError, self.auth.perform, [self.achall])
dns_test_common.write({"corenetworks_password": API_PASSWORD},
self.config.corenetworks_credentials)
self.assertRaises(errors.PluginError, self.auth.perform, [self.achall])
if __name__ == "__main__":
unittest.main()