Rename project
This commit is contained in:
parent
49da8a2602
commit
e780024140
8 changed files with 24 additions and 20 deletions
39
transip_client/__init__.py
Normal file
39
transip_client/__init__.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
env_path = Path("..") / ".env"
|
||||
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
|
||||
def get_current_version():
|
||||
if "VERSION" in os.environ:
|
||||
return os.environ["VERSION"]
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["git", "describe", "--tags"], universal_newlines=True
|
||||
)
|
||||
return output.strip()
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
return ""
|
||||
|
||||
|
||||
VERSION = get_current_version()
|
||||
|
||||
# Optionally use sentry integration
|
||||
try:
|
||||
from sentry_sdk import init as sentry_init
|
||||
|
||||
sentry_init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
send_default_pii=False,
|
||||
release=VERSION,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
21
transip_client/cli.py
Normal file
21
transip_client/cli.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import click
|
||||
|
||||
from transip_client.main import detect
|
||||
|
||||
DEFAULT_DNS = "myip.opendns.com"
|
||||
DEFAULT_DNS_NAME = "@resolver1.opendns.com"
|
||||
DEFAULT_API_URL = "https://api.transip.nl/v6"
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("domains", envvar="DOMAINS", nargs=-1)
|
||||
@click.argument("token", envvar="TOKEN")
|
||||
@click.option("--dns", envvar="DNS", default=DEFAULT_DNS)
|
||||
@click.option("--dns-name", envvar="DNS_NAME", default=DEFAULT_DNS_NAME)
|
||||
@click.option("--api-url", envvar="API_URL", default=DEFAULT_API_URL)
|
||||
@click.option("--read-only/--write", envvar="READ_ONLY", default=False)
|
||||
def run(domains, token, dns, dns_name, api_url, read_only):
|
||||
if not domains:
|
||||
raise ValueError("No domain(s) specified")
|
||||
|
||||
detect(domains, (dns, dns_name), api_url, token, read_only)
|
||||
103
transip_client/main.py
Normal file
103
transip_client/main.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_ip(resolvers):
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["dig", "+short", *resolvers],
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise OSError("Unable to retrieve current IP") from e
|
||||
|
||||
return output.decode("utf-8").strip()
|
||||
|
||||
|
||||
def _get_domain(domain, token, api_url):
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
return requests.get(f"{api_url}/domains/{domain}/dns", headers=headers)
|
||||
|
||||
|
||||
def _get_domain_data(domains, token, api_url):
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {
|
||||
executor.submit(_get_domain, domain, token, api_url): domain
|
||||
for domain in domains
|
||||
}
|
||||
|
||||
for future in as_completed(futures):
|
||||
response = future.result()
|
||||
domain = futures[future]
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
logger.exception(f"Failed retrieving information for {domain}")
|
||||
continue
|
||||
|
||||
yield {"domain": domain, **response.json()}
|
||||
|
||||
|
||||
def _update_domain(domain, payload, api_url, token):
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
return requests.put(
|
||||
f"{api_url}/domains/{domain}/dns", data=json.dumps(payload), headers=headers
|
||||
)
|
||||
|
||||
|
||||
def _update_domains(updated_domains, api_url, token, read_only):
|
||||
if read_only:
|
||||
return
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {
|
||||
executor.submit(_update_domain, domain, entries, api_url, token): domain
|
||||
for domain, entries in updated_domains.items()
|
||||
}
|
||||
|
||||
for future in as_completed(futures):
|
||||
response = future.result()
|
||||
domain = futures[future]
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except request.HTTPError as e:
|
||||
logger.exception(f"Unable to update domain {domain}")
|
||||
continue
|
||||
|
||||
logger.info(f"Updated domain {domain}")
|
||||
|
||||
|
||||
def detect(domains, resolvers, api_url, token, read_only):
|
||||
ip = _get_ip(resolvers)
|
||||
domain_data = _get_domain_data(domains, token, api_url)
|
||||
updated_domains = {}
|
||||
|
||||
for data in domain_data:
|
||||
dns_entries = data["dnsEntries"]
|
||||
domain = data["domain"]
|
||||
updated_entries = []
|
||||
|
||||
for dns_entry in dns_entries:
|
||||
updated_entries.append(
|
||||
{
|
||||
**dns_entry,
|
||||
"content": ip,
|
||||
}
|
||||
)
|
||||
|
||||
if dns_entries == updated_entries:
|
||||
continue
|
||||
|
||||
updated_domains[domain] = {"dnsEntries": updated_entries}
|
||||
|
||||
_update_domains(updated_domains, api_url, token, read_only)
|
||||
405
transip_client/tests.py
Normal file
405
transip_client/tests.py
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
import json
|
||||
|
||||
from unittest import TestCase, skipIf
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
from pkg_resources import get_distribution
|
||||
from requests import HTTPError
|
||||
|
||||
from transip_client.cli import DEFAULT_API_URL, run
|
||||
|
||||
|
||||
class RunTestCase(TestCase):
|
||||
def setUp(self):
|
||||
patcher = patch("transip_client.main.subprocess.check_output")
|
||||
self.mocked_dns = patcher.start()
|
||||
|
||||
patcher = patch("transip_client.main.requests.get")
|
||||
self.mocked_get = patcher.start()
|
||||
|
||||
patcher = patch("transip_client.main.requests.put")
|
||||
self.mocked_put = patcher.start()
|
||||
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_simple(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.return_value = {
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
with self.assertLogs("transip_client.main", level="INFO") as logger:
|
||||
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
|
||||
|
||||
self.assertEqual(
|
||||
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
|
||||
)
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
expected_json = json.dumps(
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.420",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
self.mocked_put.assert_called_with(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
data=expected_json,
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
def test_error_response(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.raise_for_status.side_effect = HTTPError
|
||||
|
||||
with self.assertLogs("transip_client.main", level="INFO") as logger:
|
||||
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
|
||||
|
||||
error_log = logger.output[0]
|
||||
|
||||
self.assertIn("Failed retrieving information for foobar.com", error_log)
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
self.mocked_put.assert_not_called()
|
||||
|
||||
def test_matching_ip(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.return_value = {
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.420",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
self.mocked_put.assert_not_called()
|
||||
|
||||
def test_readonly(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.return_value = {
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
result = self.runner.invoke(run, ["foobar.com", "TOKEN", "--read-only"])
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
self.mocked_put.assert_not_called()
|
||||
|
||||
def test_different_api_url(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.return_value = {
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
with self.assertLogs("transip_client.main", level="INFO") as logger:
|
||||
result = self.runner.invoke(
|
||||
run, ["foobar.com", "TOKEN", "--api-url", "https://other-provider.com"]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
|
||||
)
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
f"https://other-provider.com/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
expected_json = json.dumps(
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.420",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
self.mocked_put.assert_called_with(
|
||||
f"https://other-provider.com/domains/foobar.com/dns",
|
||||
data=expected_json,
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
def test_env_var(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.return_value = {
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
with self.assertLogs("transip_client.main", level="INFO") as logger:
|
||||
result = self.runner.invoke(
|
||||
run, ["foobar.com", "TOKEN"], env={"API_URL": "https://new-api.com"}
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
|
||||
)
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
self.mocked_get.assert_called_with(
|
||||
"https://new-api.com/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
expected_json = json.dumps(
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.420",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
self.mocked_put.assert_called_with(
|
||||
"https://new-api.com/domains/foobar.com/dns",
|
||||
data=expected_json,
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
)
|
||||
|
||||
# see https://click.palletsprojects.com/en/master/changelog/#version-8-0
|
||||
@skipIf(
|
||||
get_distribution("click").version < "8",
|
||||
"Multiple arguments through env variables not supported",
|
||||
)
|
||||
def test_multi_arg_env_var(self):
|
||||
self.mocked_dns.return_value = b"111.420\n"
|
||||
self.mocked_get.return_value.json.side_effect = [
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foobar.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.421",
|
||||
}
|
||||
],
|
||||
"_links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"link": "https://api.transip.nl/v6/domains/foofoo.com/dns",
|
||||
},
|
||||
{
|
||||
"rel": "domain",
|
||||
"link": "https://api.transip.nl/v6/domains/foofoo.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
with self.assertLogs("transip_client.main", level="INFO") as logger:
|
||||
result = self.runner.invoke(
|
||||
run, ["TOKEN"], env={"DOMAINS": "foobar.com foofoo.com"}
|
||||
)
|
||||
|
||||
self.assertIsNone(result.exception)
|
||||
self.assertEqual(
|
||||
logger.output,
|
||||
[
|
||||
"INFO:transip_client.main:Updated domain foobar.com",
|
||||
"INFO:transip_client.main:Updated domain foofoo.com",
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
expected_calls = [
|
||||
call(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
),
|
||||
call().raise_for_status(),
|
||||
call().json(),
|
||||
call(
|
||||
f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
),
|
||||
call().raise_for_status(),
|
||||
call().json(),
|
||||
]
|
||||
|
||||
self.mocked_get.assert_has_calls(expected_calls)
|
||||
|
||||
expected_json = json.dumps(
|
||||
{
|
||||
"dnsEntries": [
|
||||
{
|
||||
"name": "@",
|
||||
"expire": 60,
|
||||
"type": "A",
|
||||
"content": "111.420",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
expected_calls = [
|
||||
call(
|
||||
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
|
||||
data=expected_json,
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
),
|
||||
call().raise_for_status(),
|
||||
call(
|
||||
f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
|
||||
data=expected_json,
|
||||
headers={"Authorization": "Bearer TOKEN"},
|
||||
),
|
||||
call().raise_for_status(),
|
||||
]
|
||||
|
||||
self.mocked_put.assert_has_calls(expected_calls)
|
||||
|
||||
def test_no_domains(self):
|
||||
result = self.runner.invoke(run, ["TOKEN"])
|
||||
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
self.assertEqual(str(result.exception), "No domain(s) specified")
|
||||
|
||||
self.mocked_get.assert_not_called()
|
||||
self.mocked_put.assert_not_called()
|
||||
Loading…
Add table
Add a link
Reference in a new issue