From 6c04860119b1a14a6f09390cbd8dada24a6015d0 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Tue, 28 Mar 2023 16:37:38 +0200 Subject: [PATCH] feat: license, cronjob, requirements and packaging --- .gitignore | 2 ++ LICENSE | 21 +++++++++++++++++++ cron.d/netcup-dns | 11 ++++++++++ pyproject.toml | 8 +++++++ requirements.txt | 1 + setup.cfg | 35 +++++++++++++++++++++++++++++++ src/netcup_dns/__init__.py | 0 main.py => src/netcup_dns/main.py | 30 ++++++++++++++++++-------- 8 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 LICENSE create mode 100644 cron.d/netcup-dns create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 src/netcup_dns/__init__.py rename main.py => src/netcup_dns/main.py (71%) diff --git a/.gitignore b/.gitignore index d1e11b9..a96649f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /.idea/ /cfg/ /venv/ +/build/ +**/*.egg-info/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a9309e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Daniel Langbein + +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 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. diff --git a/cron.d/netcup-dns b/cron.d/netcup-dns new file mode 100644 index 0000000..8fa46f7 --- /dev/null +++ b/cron.d/netcup-dns @@ -0,0 +1,11 @@ +# Run command every 3min +# - https://crontab.guru/every-3-minutes +# `/etc/cron.d/` requires user field +# - https://unix.stackexchange.com/questions/458713/how-are-files-under-etc-cron-d-used#comment1019389_458715 +# Some users report that files in `/etc/cron.d/` containing `-` are not executed +# - https://unix.stackexchange.com/questions/296347/crontab-never-running-while-in-etc-cron-d#comment640748_296351 +# PATH is restricted to `/bin:/usr/bin` but `de-p1st-execNotify` resides in `/usr/local/bin/` +# - https://serverfault.com/a/449652 + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin/ +*/3 * * * * root de-p1st-execNotify netcup-dns > /var/log/netcup-dns.cron 2>&1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c035a4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +# https://packaging.python.org/tutorials/packaging-projects/#creating-pyproject-toml + +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index c4bd0b6..058d48e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ nc-dnsapi +requests diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1281f9d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,35 @@ +; setup.cfg is the configuration file for setuptools. +; https://packaging.python.org/tutorials/packaging-projects/#configuring-metadata + +[metadata] +name = netcup_dns +version = 0.1.0 +author = Daniel Langbein +author_email = daniel@systemli.org +description = Update DNS records with your current external IP address using the netcup DNS API. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://codeberg.org/privacy1st/netcup-dns +project_urls = + Bug Tracker = https://codeberg.org/privacy1st/netcup-dns/issues + +; https://pypi.org/classifiers/ +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: Unix + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.6.9 + +[options.packages.find] +where = src + +[options.entry_points] +; https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html +console_scripts= + netcup-dns = netcup_dns.main:main diff --git a/src/netcup_dns/__init__.py b/src/netcup_dns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/src/netcup_dns/main.py similarity index 71% rename from main.py rename to src/netcup_dns/main.py index 98e1a62..469a778 100644 --- a/main.py +++ b/src/netcup_dns/main.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- import configparser import ipaddress +from functools import lru_cache from pathlib import Path import requests @@ -8,14 +11,16 @@ from nc_dnsapi import Client, DNSRecord def main(): """ - https://github.com/nbuchwitz/nc_dnsapi + The main effort is done by https://github.com/nbuchwitz/nc_dnsapi """ - destination = external_ipv4() + cfg_dir = Path('cfg') + if not cfg_dir.exists(): + raise Exception(f'The config directory is missing: {cfg_dir}') - files = [file for file in Path('cfg').iterdir() if file.name.endswith('.cfg') and file.is_file()] - for file in files: + cfg_files = [file for file in cfg_dir.iterdir() if file.name.endswith('.ini') and file.is_file()] + for cfg_file in cfg_files: cfg = configparser.ConfigParser() - cfg.read(file) + cfg.read(cfg_file) customer = cfg['credentials']['customer'] api_key = cfg['credentials']['api_key'] api_password = cfg['credentials']['api_password'] @@ -25,8 +30,13 @@ def main(): with Client(customer, api_key, api_password) as api: for domain in domains: hostname = cfg[domain]['hostname'] - type_ = cfg[domain]['type'] - update_record_destination(api, domain, hostname, type_, destination) + type_ = cfg[domain]['type'].upper() + if type_ == 'A': + destination = external_ipv4() + update_record_destination(api, domain, hostname, type_, destination=destination) + print(f'Set {hostname}.{domain} {type_} record to {destination}') + else: + raise Exception(f'DNS record type {type_} is not supported.') def update_record_destination(api: Client, domain: str, hostname: str, type_: str, destination: str) -> None: @@ -45,8 +55,10 @@ def get_record(api: Client, domain: str, hostname: str, type_: str) -> DNSRecord return matches[0] -def external_ipv4() -> str: +@lru_cache(maxsize=None) +def external_ipv4(timeout=5) -> str: """ + :argument timeout: Timeout for each IP detection webservice in seconds. :return: Public IPv4 address """ @@ -67,7 +79,7 @@ def external_ipv4() -> str: requests.packages.urllib3.util.connection.HAS_IPV6 = False # Timeout after 5 seconds. - ip = requests.get(endpoint, timeout=5).text.strip() + ip = requests.get(endpoint, timeout=timeout).text.strip() # Check if it is actually an IPv4 address. # Some services, such as e.g. v4.ident.me, sometimes return IPv6. ipv4 = ipaddress.ip_address(ip)