From ecdcf51d91822735ee423c33ea7d038e65d55ce1 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Fri, 15 Mar 2024 17:41:37 +0100 Subject: [PATCH 1/4] collect API changes before executing them --- recordmaster/_sync_records.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/recordmaster/_sync_records.py b/recordmaster/_sync_records.py index e57e1d5..bcdbceb 100644 --- a/recordmaster/_sync_records.py +++ b/recordmaster/_sync_records.py @@ -17,10 +17,10 @@ def sync_existing_local_to_remote( api: ApiClient, domain: Domain, dry: bool, interactive: bool ) -> None: """Compare previously matched local records to remote ones. If differences, update remote""" - # pylint: disable=too-many-nested-blocks # Loop over local records which have an ID, so matched to a remote entry for loc_rec in [loc_rec for loc_rec in domain.local_records if loc_rec.id]: # For each ID, compare content, ttl and prio + changes = {} for key in ("content", "ttl", "prio"): # Get local and corresponding remote attribute loc_val = getattr(loc_rec, key) @@ -33,7 +33,7 @@ def sync_existing_local_to_remote( if loc_val and (loc_val != rem_val): # Log and update record logging.info( - "[%s] Updating '%s' record of '%s': '%s' from '%s' to '%s'", + "[%s] Update '%s' record of '%s': '%s' from '%s' to '%s'", domain.name, loc_rec.type, loc_rec.name, @@ -43,20 +43,24 @@ def sync_existing_local_to_remote( ) # Update record via API call - inwx_api( - api, - "nameserver.updateRecord", - interactive=interactive, - dry=dry, - id=loc_rec.id, - **{key: loc_val}, - ) + changes[key] = loc_val else: # No action needed as records are equal or undefined logging.debug( "[%s] (%s) %s equal: %s = %s", loc_rec.name, loc_rec.id, key, rem_val, loc_val ) + # Execute collected changes for this ID, if they exist + if changes: + inwx_api( + api, + "nameserver.updateRecord", + interactive=interactive, + dry=dry, + id=loc_rec.id, + **changes, + ) + def create_missing_at_remote( api: ApiClient, domain: Domain, records: list[Record], dry: bool, interactive: bool From b7cab15c0b625e7372c96b85c8f91fcf6d40faf9 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Fri, 15 Mar 2024 17:42:35 +0100 Subject: [PATCH 2/4] also collect attributes for URL record type --- recordmaster/__init__.py | 16 +++++++++++++++- recordmaster/_api.py | 4 ++++ recordmaster/_data.py | 20 ++++++++++++++------ recordmaster/_sync_records.py | 4 ++-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/recordmaster/__init__.py b/recordmaster/__init__.py index 540f9a4..7cf7665 100644 --- a/recordmaster/__init__.py +++ b/recordmaster/__init__.py @@ -9,7 +9,21 @@ __version__ = version("inwx-dns-recordmaster") -RECORD_KEYS = ("id", "name", "type", "content", "ttl", "prio") +# All record keys +RECORD_KEYS = ( + "id", + "name", + "type", + "content", + "ttl", + "prio", + "urlRedirectType", + "urlRedirectTitle", + "urlRedirectDescription", + "urlRedirectFavIcon", + "urlRedirectKeywords", + "urlAppend", +) DEFAULT_APP_CONFIG = """# App configuration for INWX DNS Recordmaster. # This is not the place for domain records, these can be anywhere and used with the -c flag diff --git a/recordmaster/_api.py b/recordmaster/_api.py index 1fbc707..153aa1c 100644 --- a/recordmaster/_api.py +++ b/recordmaster/_api.py @@ -86,6 +86,10 @@ def inwx_api( logging.info("API call for '%s' has not been executed in dry-run mode", method) return {} + for key, value in params.items(): + if isinstance(value, bool): + params[key] = 1 if value else 0 + api_result = api.call_api(api_method=method, method_params=params) # Handle return codes diff --git a/recordmaster/_data.py b/recordmaster/_data.py index 952b078..1884f7f 100644 --- a/recordmaster/_data.py +++ b/recordmaster/_data.py @@ -18,7 +18,7 @@ @dataclass -class Record: +class Record: # pylint: disable=too-many-instance-attributes """Dataclass holding a nameserver record, be it remote or local""" # nameserver details @@ -28,6 +28,13 @@ class Record: content: str = "" ttl: int = 3600 prio: int = 0 + # pylint: disable=invalid-name + urlRedirectType: str = "" + urlRedirectTitle: str = "" + urlRedirectDescription: str = "" + urlRedirectFavIcon: str = "" + urlRedirectKeywords: str = "" + urlAppend: bool = False def import_records(self, data: dict, domain: str = "", root: str = ""): """Update records by providing a dict""" @@ -85,11 +92,12 @@ def to_local_conf_format(self, records: list[Record], ignore_types: list) -> dic # Type and content are straightforward, we don't need to convert it rec_yaml: dict[str, str | int] = {"type": rec.type, "content": rec.content} - # TTL and prio will be set unless it's the default value - if rec.ttl != Record.ttl: - rec_yaml["ttl"] = rec.ttl - if rec.prio != Record.prio: - rec_yaml["prio"] = rec.prio + + # All the other attributes unless they have the default value + # This is, in RECORD_KEYS, all from the 5th element, ttl + for attr in RECORD_KEYS[4:]: + if getattr(rec, attr) != getattr(Record, attr): + rec_yaml[attr] = getattr(rec, attr) data[name].append(rec_yaml) diff --git a/recordmaster/_sync_records.py b/recordmaster/_sync_records.py index bcdbceb..bc9da23 100644 --- a/recordmaster/_sync_records.py +++ b/recordmaster/_sync_records.py @@ -19,9 +19,9 @@ def sync_existing_local_to_remote( """Compare previously matched local records to remote ones. If differences, update remote""" # Loop over local records which have an ID, so matched to a remote entry for loc_rec in [loc_rec for loc_rec in domain.local_records if loc_rec.id]: - # For each ID, compare content, ttl and prio + # For each ID, compare content, ttl, prio etc, collect changes, and make API call changes = {} - for key in ("content", "ttl", "prio"): + for key in RECORD_KEYS[3:]: # Get local and corresponding remote attribute loc_val = getattr(loc_rec, key) rem_val = next( From c15fb87941933bb8b216b8743cef036472c10ba4 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Fri, 15 Mar 2024 17:53:26 +0100 Subject: [PATCH 3/4] deal with False values --- recordmaster/_sync_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recordmaster/_sync_records.py b/recordmaster/_sync_records.py index bc9da23..e64bba7 100644 --- a/recordmaster/_sync_records.py +++ b/recordmaster/_sync_records.py @@ -30,7 +30,7 @@ def sync_existing_local_to_remote( if rem_rec.id == loc_rec.id ) # Update attribute at remote if values differ - if loc_val and (loc_val != rem_val): + if loc_val != rem_val: # Log and update record logging.info( "[%s] Update '%s' record of '%s': '%s' from '%s' to '%s'", From b2bdc7c6c6ae9c8035bacf4e9ed18141991c8153 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Mon, 18 Mar 2024 16:02:03 +0100 Subject: [PATCH 4/4] add documentation for URL records --- README.md | 6 ++++++ recordmaster/_api.py | 1 + 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 54f8bb2..681f9f1 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Note: This is no official software project by INWX, it just kindly uses their pu - [Debug and dry-run](#debug-and-dry-run) - [I deleted all my productive records!](#i-deleted-all-my-productive-records) - [Simulate API response](#simulate-api-response) + - [URL records](#url-records) - [License](#license) @@ -216,6 +217,11 @@ This could look like the following: In order to get this output from an existing domain, you can run the program with the `--debug` flag and search for the line starting with `Response (nameserver.info):`. +### URL Records + +INWX has [URL records](https://kb.inwx.com/en-us/3-nameserver/106-how-can-i-forward-a-domain-to-an-internet-address) that allow for redirections of a domain to another domain. These records are somewhat supported by this tool, but there are [several issues and bugs](https://github.com/mxmehl/inwx-dns-recordmaster/pull/23). It's recommended to add and edit these records in the web interface and not via this tool as the INWX API doesn't seem to be reliable, but you can still store these configurations locally. + + ## License The main license of this project is the GNU General Public License 3.0, no later version (`GPL-3.0-only`), Copyright Max Mehl. diff --git a/recordmaster/_api.py b/recordmaster/_api.py index 153aa1c..7eee74d 100644 --- a/recordmaster/_api.py +++ b/recordmaster/_api.py @@ -86,6 +86,7 @@ def inwx_api( logging.info("API call for '%s' has not been executed in dry-run mode", method) return {} + # Convert boolean values to 0/1 as this is what the INWX seems to expect for key, value in params.items(): if isinstance(value, bool): params[key] = 1 if value else 0