Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for German DNS provider Core Networks #494

Open
wants to merge 98 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
0958442
Initial provider for Core Networks
MasinAD Apr 12, 2020
e207e99
Fixed corenetwork:Provider._update_record
MasinAD Apr 24, 2020
e6f383e
Compatibility fix for Python 2.7
MasinAD Apr 24, 2020
dd0ba44
Recording missed a call
MasinAD Apr 24, 2020
23878ca
Changed Exception message to what Certbot's dns_common_lexicon.py exp…
MasinAD Apr 26, 2020
e10f6c2
Merge branch 'master' into pr/494
adferrand Jun 1, 2020
98821c1
integrated Logging
MasinAD Jan 7, 2021
b1ab75d
Merge pull request #1 from AnalogJ/master
MasinAD Jan 7, 2021
5bc660d
removed some logging messages, added logic for failing gracefully in …
MasinAD Jan 8, 2021
2b8bb14
Added some clarification to the auth-file parameter help.
MasinAD Jan 8, 2021
421460d
Removed commented out code in provider test.
MasinAD Jan 8, 2021
52977c0
Switched to contextmanager for committing changes
MasinAD Jan 8, 2021
24c5a8f
Merge branch 'master' into pr/494
adferrand Aug 9, 2021
b05a8fd
Relax dependencies
adferrand Oct 3, 2021
198385c
Update dependencies and fix doc
adferrand Oct 3, 2021
8d2853a
Changelog
adferrand Oct 3, 2021
81080b4
Version 3.7.1
adferrand Oct 3, 2021
3ae7d45
Troubleshoot issues with Azure (#971)
adferrand Oct 4, 2021
58f8990
Add changelog
adferrand Oct 4, 2021
c0d4221
Version 3.8.0
adferrand Oct 4, 2021
0862b17
Update dependencies
adferrand Oct 12, 2021
8f90a01
Fix invalid API request to Rackspace Cloud DNS - Issue #981 (#989)
mattgauf Oct 15, 2021
fae4b77
Prepare changelog
adferrand Oct 15, 2021
243c008
Version 3.8.1
adferrand Oct 15, 2021
48e8421
Update dreamhost _authenticate to use dns-list_records instead of dom…
ryan953 Nov 3, 2021
3f4dfca
Prepare changelog
adferrand Nov 3, 2021
1765243
Version 3.8.2
adferrand Nov 3, 2021
e965bee
Update dependencies
adferrand Nov 3, 2021
531c37c
Update dependencies
adferrand Nov 11, 2021
aac90a1
Update windows environments
adferrand Nov 11, 2021
4214b64
Update dependencies and changelog
adferrand Nov 12, 2021
ce25e25
Fix find_site, dataset is not a valid child of filter, but a child of…
spike77453 Nov 11, 2021
cb87590
🐛 update Namecheap's nameserver list (#911)
dudeofawesome Nov 11, 2021
a1c414f
Version 3.8.3
adferrand Nov 12, 2021
8b72bf5
Update CHANGELOG.md
adferrand Nov 12, 2021
2b3a0ec
Update of the netcup nameserver (#1016)
mustermann2021 Nov 14, 2021
e1ccb12
Update Nameserver for INWX (#1015)
mustermann2021 Nov 14, 2021
b5daac1
Support & test Python 3.10 (#1017)
adferrand Nov 14, 2021
f65991e
Update dependencies
adferrand Nov 18, 2021
bd1905f
Take description of the provider into account in the doc
adferrand Nov 24, 2021
d8e2b4d
Fix return
adferrand Nov 24, 2021
fd8c361
Ensure TTL is provided as an integer (#1031)
parthjoshi-pc Nov 25, 2021
23c521c
Update dependencies
adferrand Dec 28, 2021
4719982
Fix tox config
adferrand Dec 28, 2021
c8d15b7
Fix tox deps
adferrand Dec 28, 2021
734ff83
Short fix for godaddy
adferrand Dec 28, 2021
39fe561
Fix lint
adferrand Dec 28, 2021
2753694
Add support for value-domain.com (#1018)
Dec 28, 2021
bb97f03
Prepare changelog
adferrand Dec 28, 2021
3bd9213
Update documentation
adferrand Dec 28, 2021
26ba265
Version 3.8.4
adferrand Dec 28, 2021
06c04ac
Complete redesign of the update logic in godaddy provider
adferrand Dec 29, 2021
1dce378
Prepare changelog
adferrand Dec 29, 2021
7cb82ad
Version 3.8.5
adferrand Dec 29, 2021
c508037
Factor common logic in godaddy
adferrand Dec 29, 2021
04f233f
Rewrite delete logic in godaddy provider
adferrand Dec 29, 2021
696309b
Version 3.8.6
adferrand Dec 29, 2021
e6ddbe1
Revert failed versions
adferrand Dec 29, 2021
d117b77
Version 3.8.5
adferrand Dec 29, 2021
c0694ff
Drop Python 3.6 support
adferrand Jan 6, 2022
c2b409f
Update dependencies
adferrand Jan 6, 2022
c1b6078
Version 3.9.0
adferrand Jan 6, 2022
6b96a7a
added Porkbun (#1062)
juanalbglz Jan 9, 2022
b1748e2
Update dependencies
adferrand Jan 9, 2022
2c5f784
fix conoha provider (specify auth_region when get provider option) (#…
k-serenade Jan 9, 2022
65cb705
Exclude integration tests from source distributions + remove MANIFEST…
sbraz Jan 9, 2022
509223b
Use known domain as filter (#954)
dgrothaus-ku Jan 9, 2022
443e2cb
Prepare build of version doc
adferrand Jan 17, 2022
3c4b376
Reimplement transip provider with the API REST v6 (#1086)
adferrand Jan 17, 2022
1be1d8d
Prepare changelog
adferrand Jan 17, 2022
137527f
Version 3.9.1
adferrand Jan 17, 2022
ac8a57a
Update documentation
adferrand Jan 17, 2022
7a8470a
Reorganize documentation
adferrand Jan 17, 2022
1b1372c
Prepare changelog
adferrand Jan 17, 2022
6e82b5e
Version 3.9.2
adferrand Jan 17, 2022
32a6fd5
Use appropriate JSONDecodeError for retrocompatibility purposes (#1100)
adferrand Jan 26, 2022
1e18d4e
Prepare changelog
adferrand Jan 26, 2022
877afc0
Version 3.9.3
adferrand Jan 26, 2022
e7f6e60
Bump boto3 from 1.20.44 to 1.20.53 (#1124)
dependabot[bot] Feb 13, 2022
3092ad0
Bump types-toml from 0.10.3 to 0.10.4 (#1123)
dependabot[bot] Feb 13, 2022
766561b
Bump types-requests from 2.27.7 to 2.27.9 (#1121)
dependabot[bot] Feb 13, 2022
6fbe201
Bump oci from 2.55.0 to 2.56.0 (#1120)
dependabot[bot] Feb 13, 2022
99b3c2c
Bump types-setuptools from 57.4.7 to 57.4.9 (#1116)
dependabot[bot] Feb 13, 2022
f84c1bd
Bump softlayer from 5.9.8 to 5.9.9 (#1115)
dependabot[bot] Feb 13, 2022
78a8fd6
Bump pytest from 6.2.5 to 7.0.0 (#1114)
dependabot[bot] Feb 13, 2022
4db0a0f
Updated valid record types for DreamHost integration (#1110)
JerrettDavis Feb 13, 2022
6ca8dbe
Bump black from 21.12b0 to 22.1.0 (#1107)
dependabot[bot] Feb 13, 2022
fb5004e
Bump types-pyyaml from 6.0.3 to 6.0.4 (#1105)
dependabot[bot] Feb 13, 2022
bb533ff
Bump dnspython from 2.1.0 to 2.2.0 (#1089)
dependabot[bot] Feb 13, 2022
f78cd4f
Update dependencies
adferrand Feb 13, 2022
1c10cc5
Add Webgo Provider (#1102)
mod242 Feb 13, 2022
e969090
Update documentation
adferrand Feb 13, 2022
58508b3
Prepare changelog
adferrand Feb 14, 2022
61e26a6
Version 3.9.4
adferrand Feb 14, 2022
314ab08
Merge branch 'master' of https://github.com/AnalogJ/lexicon
MasinAD Mar 29, 2022
219455d
removed credentials
MasinAD Mar 29, 2022
02c0aac
Fixed corenetworks provider not authenticating in tests
MasinAD Mar 30, 2022
06d2c30
Merge branch 'master' into pr/494
adferrand Apr 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ lexicon/providers/cloudns.py @ppmathis
lexicon/providers/cloudxns.py @zh99998
lexicon/providers/conoha.py @kaz
lexicon/providers/constellix.py @pmkane
lexicon/providers/corenetworks.py @masinad
lexicon/providers/ddns.py @trinity-1686a
lexicon/providers/digitalocean.py @analogj @foxwoods369
lexicon/providers/dinahosting.py @iperurena
Expand Down
326 changes: 326 additions & 0 deletions lexicon/providers/corenetworks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
"""Module provider for Core Networks"""
from __future__ import absolute_import
import json
import hashlib
import logging
import time
import os
import tempfile

import requests
from lexicon.providers.base import Provider as BaseProvider
from contextlib import contextmanager

CorenetworksLog = logging.getLogger(__name__)
fh = logging.FileHandler('corenetworks.debug.log')
fh.setLevel(logging.DEBUG)
CorenetworksLog.addHandler(fh)
#CorenetworksLog.setLevel(logging.DEBUG)

NAMESERVER_DOMAINS = ['core-networks.de', 'core-networks.eu', 'core-networks.com']

def provider_parser(subparser):
"""Configure provider parser for Core Networks"""
subparser.add_argument(
"--auth-username", help="Specify login for authentication")
subparser.add_argument(
"--auth-password", help="Specify password for authentication")
# subparser.add_argument(
# "--auth-file", help="Specify location for authentication file. If this contains a valid token it will be used. Otherwise --auth-username and --auth-password are necessary. Defaults to ~/corenetworks_auth.json if ~ is writable by lexicon, otherwise $TMP/corenetworks_auth.json. In most cases you don't need to specify it as it will be used by default before acquiring a new token from the provider. The token has a lifetime of 1 hour after which it must be re-issued using the credentials. The most common use-case would be to consistently set this to some secure location where only lexicon has access. Might be deprecated in the future.")

class _CommitTrigger:
def __init__(self):
self._need_commit = False

@property
def need_commit(self):
return self._need_commit

@need_commit.setter
def need_commit(self, set_need_commit):
CorenetworksLog.debug("Entering need.commit(set_need_commit = %s)" % set_need_commit)
self._need_commit = set_need_commit


class Provider(BaseProvider):
@contextmanager
def ensure_commit(self):
commit_trigger = _CommitTrigger()
try:
CorenetworksLog.info("Still working.")
yield commit_trigger
finally:
if commit_trigger.need_commit:
CorenetworksLog.info("Finalizing commits.")
self._post("/dnszones/{0}/records/commit".format(self.domain))

"""Provider class for Core Networks"""
def __init__(self, config):
CorenetworksLog.info("Initialising class Provider")
super(Provider, self).__init__(config)
self.domain_id = None # zone name
self.account_id = None # unused?
self.token = None # provided by service after auth
self.expiry = None # token expiry time, calculated after auth
self.auth_file = { 'token': None, 'expiry': None }
# Core Networks enforces a limit on the amount of logins per minute.
# As the token is valid for 1 hour it's sensible to store it for
# later usage.
if os.path.exists(os.path.expanduser("~")) and os.path.expanduser("~") != '':
path = os.path.expanduser("~")
else:
path = tempfile.gettempdir()
# self.auth_file_path = self._get_provider_option('auth_file') or (path+'/corenetworks_auth.json')
self.auth_file_path = (path+'/corenetworks_auth.json')
self.api_endpoint = 'https://beta.api.core-networks.de'

def _authenticate(self):
"""Authenticate by either providing stored access token or
acquiring and storing token. This method will query the
list of zones and store them for later use. If the requested
domain is not in the list of zones it will raise an exception.
Ref: https://beta.api.core-networks.de/doc/#functon_auth_token"""
CorenetworksLog.debug("Entering _authenticate, requesting domain %s" % self.domain)
self._retrieve_auth_file()
CorenetworksLog.info("Value of self.auth_file: %s" % self.auth_file)
if 'token' in self.auth_file:
self.token = self.auth_file['token']
self.expiry = self.auth_file['expiry']

# Fetch new auth token if expiry is less than 60 seconds away
# as it can be we have to wait up to 60 seconds because of
# provider rate limiting.
if self.expiry-time.time() < 60:
self._get_token()
else:
self._get_token()

CorenetworksLog.info("self.expiry is %s" % self.expiry)
# Store zones for saving one API call
self.zones = self._list_zones()

#Check if requested zone is in zones list
zone = next((zone for zone in self.zones if zone["name"] == self.domain), None)
if not zone:
raise Exception('No domain found like %s.' % self.domain)
else:
self.domain_id = zone['name']
return True


def _list_records(self, rtype=None, name=None, content=None):
"""List all records. Return an empty list if no records found
type, name and content are used to filter records.
If possible filter during the query, otherwise filter after response is received.
Ref: https://beta.api.core-networks.de/doc/#functon_dnszones_records"""
CorenetworksLog.debug("Entering _list_records")
zone = next((zone for zone in self.zones if zone["name"] == self.domain), None)
if not zone:
raise Exception('Domain not found')
query_params = {}
if rtype:
query_params['type'] = rtype
if name:
query_params['name'] = self._relative_name(name)
if content:
query_params['data'] = content
payload = self._get("/dnszones/{0}/records/".format(self.domain), query_params)
for record in payload:
record['content'] = record.pop('data')
record['name'] = self._full_name(record['name'])
# Core Networks' API does not provide unique IDs for each record
# so we generate them ourselves.
record['id'] = self._make_identifier( rtype = record['type'], name = record['name'], content = record['content'] )
return payload

def _create_record(self, rtype, name, content):
"""Creates a record. If record already exists with the same content, do nothing."""
CorenetworksLog.debug("Entering _create_record")

# Check for existence of record.
existing_records = self._list_records(rtype, name, content)
new_record_id = self._make_identifier(rtype, self._full_name(name), content)
record = next((r for r in existing_records if r["id"] == new_record_id), None)
# Nothing to do if true.
if record:
return True

with self.ensure_commit() as commit_trigger:
data = {
'name': self._relative_name(name),
'data': content,
'type': rtype
}
if self._get_lexicon_option('ttl'):
data['ttl'] = self._get_lexicon_option('ttl')
# Bug reported by chkpnt. If ttl is less than 60s the API throws a "415 Client Error: Unsupported Media Type"
if data['ttl'] < 60:
data['ttl'] = 60
if self._get_lexicon_option('priority'):
data['priority'] = self._get_lexicon_option('priority')

payload = self._post("/dnszones/{0}/records/".format(self.domain), data)
# Changes to the zone need to be committed.
# https://beta.api.core-networks.de/doc/#functon_dnszones_commit
# self.modified = True
commit_trigger.need_commit = True

return new_record_id

def _update_record(self, identifier, rtype=None, name=None, content=None):
"""Updates a record. Core Networks neither supports updating a record nor is able to reliably identify a record
after a change. The best we can do is to identify the record by ourselves, fetch its data, delete it and
re-create it."""
CorenetworksLog.debug("Entering _update_record")
if identifier is not None:
# Check for existence of record
existing_records = self._list_records(rtype)
record = next((r for r in existing_records if r["id"] == identifier), None)
if not record:
return True
if rtype:
record['type'] = rtype
if name:
record['name'] = self._relative_name(name)
if content:
record['content'] = content
if self._delete_record(identifier):
new_id = self._create_record(rtype = record['type'], name = record['name'], content = record['content'])
return new_id
else:
records = self._list_records( rtype=rtype, name=self._relative_name(name) )
if len(records) > 0:
if len(records) > 1:
CorenetworksLog.warning("Found %s records, will only update the first record in search result list." % len(records))
record = records[0]
return self._update_record( record['id'], rtype, name, content )
else:
return True
return False

def _delete_record(self, identifier=None, rtype=None, name=None, content=None):
"""Delete an existing record.
If record does not exist, do nothing.
Ref: https://beta.api.core-networks.de/doc/#functon_dnszones_records_delete"""
CorenetworksLog.debug("Entering _delete_record")
with self.ensure_commit() as commit_trigger:
if identifier is not None:
# Check for existence of record
existing_records = self._list_records( rtype, name, content )
record = next((r for r in existing_records if r["id"] == identifier), None)
if not record:
return True
data = {
'name': self._relative_name(record['name']),
'data': record['content'],
'type': record['type']
}
payload = self._post("/dnszones/{0}/records/delete".format(self.domain), data)
# Changes to the zone need to be committed.
# https://beta.api.core-networks.de/doc/#functon_dnszones_commit
commit_trigger.need_commit = True
else:
records = self._list_records( rtype, name, content)
if len(records) > 0:
for record in records:
self._delete_record(identifier = record['id'], rtype = record['type'], name = record['name'], content = record['content'] )
else:
return True
return True

# Helpers

def _request(self, action='GET', url='/', data=None, query_params=None):
CorenetworksLog.debug("Entering _request")
if data is None:
data = {}
if query_params is None:
query_params = {}

CorenetworksLog.info( "url: %s with data %s and query_params %s" % ( url, str(data), str(query_params) ) )
default_headers = {}

if self.token:
default_headers['Authorization'] = "Bearer {0}".format(self.token)

response = requests.request(action,
self.api_endpoint + url,
params=query_params,
data=json.dumps(data),
headers=default_headers,
)
# if the request fails for any reason, throw an error.
response.raise_for_status()
if response.text and response.json() is None:
raise Exception('No data returned')

return response.json() if response.text else None

def _list_zones(self):
"""List existing zones.
Ref: https://beta.api.core-networks.de/doc/#functon_dnszones"""
CorenetworksLog.debug("Entering _list_zones")
return self._get('/dnszones/')

def _make_identifier(self, rtype, name, content):
return hashlib.sha1('/'.join([ rtype, name, content ]).encode('utf-8')).hexdigest()

def _retrieve_auth_file(self):
"""Retrieve token and zones from json file"""
# I guess the correct way would be multiple nested checks for
# existence of path, checking if path is a file, checking if
# file is readable and so on, and each one with corresponding
# exceptions.
if(os.path.exists(self.auth_file_path) and os.path.isfile(self.auth_file_path)):
try:
auth = open(self.auth_file_path, "r")
if auth.mode == "r":
self.auth_file = json.loads(auth.read())
auth.close()
return True
else:
auth.close()
return False
except FileNotFoundError as e:
CorenetworksLog.info("No stored authentication found: %s. Acquiring token via API call." % os.strerror(e.errno))
self._get_token()
return True
else:
self._get_token()
return True

def _commit_auth_file(self):
"""Store authentication into json file."""
try:
auth = open(self.auth_file_path, "w")
if auth.mode == "w":
content = json.dumps(self.auth_file)
auth.write(content)
os.chmod(self.auth_file_path, 0o600)
return True
else:
return False
except IOError as e:
CorenetworksLog.warning("Could not write authentication file: %s" % os.strerror(e.errno))
finally:
auth.close()

def _get_token(self):
"""Request new token via API call"""
CorenetworksLog.debug("Entering _get_token.")
if self._get_provider_option('auth_username') == None or self._get_provider_option('auth_password') == None:
raise Exception("No valid authentication mechanism found")
else:
data = {
'login' : self._get_provider_option('auth_username'),
'password': self._get_provider_option('auth_password')
}
payload = self._post('/auth/token', data = data)
self.token = payload['token']
self.expiry = payload['expires'] + time.time()

# Prepare auth file and commit changes
self.auth_file['token'] = self.token
self.auth_file['expiry'] = self.expiry
self._commit_auth_file()

37 changes: 37 additions & 0 deletions lexicon/tests/providers/test_corenetworks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Test for one implementation of the interface
from unittest import TestCase

from lexicon.tests.providers.integration_tests import IntegrationTestsV2


# Hook into testing framework by inheriting unittest.TestCase and reuse
# the tests which *each and every* implementation of the interface must
# pass, by inheritance from define_tests.TheTests
class CorenetworksProviderTests(TestCase, IntegrationTestsV2):
"""Integration tests for Core Networks provider
shell environment requires
* LEXICON_CORENETWORKS_AUTH_USERNAME
* LEXICON_CORENETWORKS_AUTH_PASSWORD
* LEXICON_CORENETWORKS_API_ENDPOINT"""
provider_name = 'corenetworks'
domain = 'fluchtkapsel.de'
# endpoint = 'https://beta.api.core-networks.de'

def _filter_post_data_parameters(self):
return ['login', 'password']

def _filter_headers(self):
return ['Authorization']

def _filter_query_parameters(self):
return ['secret_key']

def _filter_response(self, response):
"""See `IntegrationTests._filter_response` for more information on how
to filter the provider response."""
return response

# def _test_parameters_overrides(self):
# return {
# 'api_endpoint': 'https://beta.api.core-networks.de',
# }
Loading