summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--requirements.txt1
-rwxr-xr-xsimpleddns.py72
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: