From e9a30b4ebc29d9afab76fb435c3bef94b8ba83e6 Mon Sep 17 00:00:00 2001 From: pommicket Date: Sun, 31 Aug 2025 16:44:26 -0400 Subject: Start DigitalOcean --- requirements.txt | 1 + simpleddns.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index 315884a..e571ccb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests ~= 2.32.5 boto3 ~= 1.40.21 +pydo ~= 0.15.0 diff --git a/simpleddns.py b/simpleddns.py index 5f9dfe4..1665c56 100755 --- a/simpleddns.py +++ b/simpleddns.py @@ -60,24 +60,43 @@ def setup_config(config_path: Path) -> None: get_ip = default_get_ip if any(c.isspace() for c in domain_name): fatal_error('Domain name must not contain any whitespace') - print('1. Linode Domains') - print('2. AWS Route 53') - domain_type = input('Select a domain type from the list above [1-2]: ').strip() + print('1. AWS Route 53') + print('2. DigitalOcean Domains') + print('3. Linode Domains') + domain_type = input('Select a domain type from the list above [1-3]: ').strip() if domain_type == '1': - access_token = input('Enter personal access token: ') - options = f'''# Personal access token (secret!!) - access_token = {repr(access_token)}''' - type_name = 'linode' - elif domain_type == '2': type_name = 'aws_route53' options = '' print('(Credentials in ~/.aws/credentials will be used)') + elif domain_type == '2': + access_token = input('Enter personal access token (will be echoed): ').strip() + if not access_token: + fatal_error('Personal access token is required.') + options = f'''# Personal access token (secret!!) + access_token = {repr(access_token)}''' + type_name = 'digitalocean' + elif domain_type == '3': + access_token = input('Enter personal access token (will be echoed): ').strip() + if not access_token: + fatal_error('Personal access token is required.') + options = f'''# Personal access token (secret!!) + access_token = {repr(access_token)}''' + type_name = 'linode' else: - print('Invalid choice') - return + fatal_error('Invalid choice') fd = os.open(config_path, os.O_CREAT | os.O_WRONLY | os.O_EXCL, 0o600) with os.fdopen(fd, 'w') as config: - config.write(f'''Settings + config.write(f'''# simpleddns configuration +# Values here are python literals (see ast.literal_eval). +# So quotes are always required around strings. +# +# Using environment variables +# If a value starts with $, it is interpreted as an environment variable, e.g. +# access_token = $MY_ACCESS_TOKEN +# In this case, simpleddns will fail if the variable isn't set, and +# to avoid ambiguity the variable's value must still be a python literal! +# So set MY_ACCESS_TOKEN='"foo"', rather than MY_ACCESS_TOKEN=foo +Settings # Interval in seconds between checking IP address for changes # (API calls are only made when it changes) interval = 15 @@ -352,6 +371,32 @@ class LinodeDomain(Domain): # OK return '' +class DigitalOceanDomain(Domain): + '''Domain registered with DigitalOcean Domains''' + access_token: str + def _init(self) -> None: + pass + def update(self, ips: list[str]) -> bool: + print('TODO',self.access_token) + return True + def validate_specifics(self) -> str: + if not getattr(self, 'access_token', ''): + return 'Access token not set' + # OK + return '' + +def _parse_config_value(value: str) -> Any: + value = value.strip() + if value.startswith('$'): + env_var = os.getenv(value[1:]) + if env_var is None: + fatal_error(f'Environment variable not set (but used in config): {value}') + value = env_var + try: + return ast.literal_eval(value) + except ValueError: + fatal_error(f'Invalid option value: {value} (try adding quotes around it?)') + def parse_config(config_path: Path) -> tuple[Settings, list[Domain]]: '''Parse configuration file''' curr_section: Settings | Domain | None = None @@ -376,9 +421,10 @@ def parse_config(config_path: Path) -> tuple[Settings, list[Domain]]: 'two space-separated arguments (type and name)') kind = parts[1] domain_name = parts[2].rstrip('.') - domain_class = { + domain_class: Optional[type] = { 'linode': LinodeDomain, 'aws_route53': Route53Domain, + 'digitalocean': DigitalOceanDomain, }.get(kind) if domain_class is None: fatal_error(f'No such domain type: {kind}') @@ -390,7 +436,7 @@ def parse_config(config_path: Path) -> tuple[Settings, list[Domain]]: if len(parts) != 2: fatal_error(f'Invalid syntax (want key = value): {line}') [key, value] = parts - setattr(curr_section, key, ast.literal_eval(value)) + setattr(curr_section, key, _parse_config_value(value)) if isinstance(curr_section, Domain): domains.append(curr_section) for domain in domains: -- cgit v1.2.3