summaryrefslogtreecommitdiff
path: root/create.py
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-05-03 20:52:54 -0700
committerElizabeth Hunt <me@liz.coffee>2025-05-03 21:29:25 -0700
commit7106b304324b3a7c4dc5fa25432f08041cbc41cb (patch)
treecac71c9d85ba0045a735a4709573634c52f38e03 /create.py
parentae2d222ab5423bef0fc136e0aee2b4243db03b17 (diff)
downloadinfra-7106b304324b3a7c4dc5fa25432f08041cbc41cb.tar.gz
infra-7106b304324b3a7c4dc5fa25432f08041cbc41cb.zip
Move to unbound
Diffstat (limited to 'create.py')
-rwxr-xr-xcreate.py151
1 files changed, 86 insertions, 65 deletions
diff --git a/create.py b/create.py
index 258b998..c956f1b 100755
--- a/create.py
+++ b/create.py
@@ -15,10 +15,10 @@ from functools import cache
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
+
class Config:
SECRETS = Path("secrets.enc")
ROOT_DOMAIN = "liz.coffee"
- PIHOLE = "https://dns.liz.coffee/api"
CLOUDFLARE = "https://api.cloudflare.com/client/v4"
INVENTORY = Path("inventory")
@@ -28,21 +28,24 @@ class Config:
GROUP_VARS = Path("group_vars/")
NGINX_SITES_ENABLED = ANSIBLE_ROLES / Path("outbound/templates/proxy/nginx/conf.d")
- INTERNAL_LOADBALANCER_HOST = "floating.home.arpa"
+ LABDNS_GROUP_VARS = GROUP_VARS / Path("labdns.yml")
+ LABDNS_DELIMITER = "internal_services:" + "\n"
EXTERNAL_LOADBALANCER_HOST = "outbound.liz.coffee"
- LOADBALANCER_INVENTORY_LINE = textwrap.dedent("""\
+ LOADBALANCER_INVENTORY_LINE = textwrap.dedent(
+ """\
swarm-one ansible_host=10.128.0.201 ansible_user=serve \
ansible_connection=ssh ansible_become_password='{{ swarm_become_password }}'
- """).strip()
+ """
+ ).strip()
@staticmethod
@cache
def read_secrets() -> Dict[str, str]:
logger.info("Reading secrets...")
result = subprocess.run(
- ['ansible-vault', 'view', str(Config.SECRETS)],
+ ["ansible-vault", "view", str(Config.SECRETS)],
stdout=subprocess.PIPE,
- stderr=subprocess.PIPE
+ stderr=subprocess.PIPE,
)
secrets = result.stdout.decode().splitlines()
logger.info("Finished reading secrets.")
@@ -53,14 +56,8 @@ class Config:
return Config.read_secrets().get(name, "")
-class IDns(ABC):
- @abstractmethod
- def put_cname(self, of: str, to: str) -> bool:
- pass
-
-
@dataclass
-class CloudflareDns(IDns):
+class CloudflareDns:
api_token: str
zone_id: str
api_url: str
@@ -69,14 +66,9 @@ class CloudflareDns(IDns):
url = f"{self.api_url}/zones/{self.zone_id}/dns_records"
headers = {
"Authorization": f"Bearer {self.api_token}",
- "Content-Type": "application/json"
- }
- data = {
- "type": "CNAME",
- "name": of,
- "content": to,
- "ttl": 3600
+ "Content-Type": "application/json",
}
+ data = {"type": "CNAME", "name": of, "content": to, "ttl": 3600}
logger.info(f"Putting Cloudflare CNAME {of} -> {to}")
response = requests.post(url, headers=headers, json=data)
if response.ok:
@@ -87,26 +79,29 @@ class CloudflareDns(IDns):
@dataclass
-class PiholeDns(IDns):
- web_password: str
- api_url: str
-
- def __authenticate(self) -> Optional[str]:
- response = requests.post(f"{self.api_url}/auth", json={"password": self.web_password})
- return response.json().get("session", {}).get("sid")
-
- def put_cname(self, of: str, to: str) -> bool:
- sid = self.__authenticate()
- if not sid:
- logger.error("Pi-hole authentication failed.")
+class HomelabDns:
+ group_vars: Path
+ delimiter: str
+
+ def put_internal(self, dns_prefix: str) -> bool:
+ new_lines = []
+ with self.group_vars.open("r") as f:
+ lines = f.readlines()
+ try:
+ internal_services = lines.index(self.delimiter)
+ except ValueError:
+ logger.error(f"Cannot find delimiter {self.delimiter} in {self.group_vars}")
+ return False
+ new_lines = (
+ lines[0:internal_services + 1]
+ + [f" - {dns_prefix}\n"]
+ + lines[internal_services + 1:]
+ )
+ if not new_lines:
return False
- url = f"{self.api_url}/config/dns/cnameRecords/{of},{to}?sid={sid}"
- response = requests.put(url)
- if response.ok:
- logger.info("Pi-hole DNS update successful.")
- return True
- logger.error(f"Pi-hole DNS update failed: {response.text}")
- return False
+ with self.group_vars.open("w") as f:
+ f.write("".join(new_lines))
+ return True
class RoleGenerator:
@@ -126,7 +121,9 @@ class RoleGenerator:
def create_tasks(self):
self.tasks_path.mkdir(parents=True, exist_ok=True)
task_file = self.tasks_path / "main.yml"
- task_file.write_text(textwrap.dedent(f"""\
+ task_file.write_text(
+ textwrap.dedent(
+ f"""\
---
- name: Deploy {self.service}
@@ -135,12 +132,16 @@ class RoleGenerator:
service_name: {self.service}
template_render_dir: "../templates"
service_destination_dir: "{{{{ {self.service}_base }}}}"
- """))
+ """
+ )
+ )
def create_compose_template(self):
(self.templates_path / "stacks").mkdir(parents=True, exist_ok=True)
compose_file = self.templates_path / "stacks" / "docker-compose.yml"
- compose_file.write_text(textwrap.dedent(f"""\
+ compose_file.write_text(
+ textwrap.dedent(
+ f"""\
services:
{self.service}:
image: {self.image}
@@ -156,15 +157,15 @@ class RoleGenerator:
timeout: 15s
interval: 30s
retries: 3
- start_period: 10s
+ start_period: 5s
deploy:
mode: replicated
update_config:
parallelism: 1
failure_action: rollback
order: start-first
- delay: 10s
- monitor: 45s
+ delay: 5s
+ monitor: 30s
replicas: 1
labels:
- traefik.enable=true
@@ -178,16 +179,22 @@ class RoleGenerator:
networks:
proxy:
external: true
- """))
+ """
+ )
+ )
def create_group_vars(self):
path = Config.GROUP_VARS / f"{self.service}.yml"
- path.write_text(textwrap.dedent(f"""\
+ path.write_text(
+ textwrap.dedent(
+ f"""\
---
{self.service}_domain: {self.service}.{Config.ROOT_DOMAIN}
{self.service}_base: "{{{{ swarm_base }}}}/{self.service}"
- """))
+ """
+ )
+ )
def create_volumes(self):
(self.templates_path / "volumes" / "data").mkdir(parents=True, exist_ok=True)
@@ -195,7 +202,9 @@ class RoleGenerator:
def create_deploy_hook(self):
path = Config.ANSIBLE_PLAYBOOKS / f"{self.service}.yml"
- path.write_text(textwrap.dedent(f"""\
+ path.write_text(
+ textwrap.dedent(
+ f"""\
---
- name: {self.service} setup
@@ -203,11 +212,15 @@ class RoleGenerator:
become: true
roles:
- {self.service}
- """))
+ """
+ )
+ )
with open(Config.ANSIBLE_DEPLOY, "a") as f:
f.write("\n")
f.write(f"- name: {self.service}\n")
- f.write(f" ansible.builtin.import_playbook: playbooks/{self.service}.yml\n")
+ f.write(
+ f" ansible.builtin.import_playbook: playbooks/{self.service}.yml\n"
+ )
def create_all(self):
self.create_inventory()
@@ -220,7 +233,9 @@ class RoleGenerator:
def create_nginx_conf(service_name: str):
path = Config.NGINX_SITES_ENABLED / f"{service_name}.conf"
- path.write_text(textwrap.dedent(f"""\
+ path.write_text(
+ textwrap.dedent(
+ f"""\
server {{
listen 80;
server_name {service_name}.liz.coffee;
@@ -240,16 +255,18 @@ def create_nginx_conf(service_name: str):
proxy_set_header Connection "upgrade";
}}
}}
- """))
+ """
+ )
+ )
def main():
parser = argparse.ArgumentParser(description="Service initializer for liz.coffee.")
- parser.add_argument('--service-name', type=str, required=True)
- parser.add_argument('--container-image', type=str, required=True)
- parser.add_argument('--service-port', type=str, default='80')
- parser.add_argument('--external', action='store_true')
- parser.add_argument('--internal', action='store_true')
+ parser.add_argument("--service-name", type=str, required=True)
+ parser.add_argument("--container-image", type=str, required=True)
+ parser.add_argument("--service-port", type=str, default="80")
+ parser.add_argument("--external", action="store_true")
+ parser.add_argument("--internal", action="store_true")
args = parser.parse_args()
logger.info(f"Initializing service setup for '{args.service_name}'")
@@ -272,16 +289,20 @@ def main():
logger.info("External DNS (Cloudflare) CNAME created successfully.")
if args.internal:
- logger.info("Configuring internal DNS via Pi-hole...")
- pi = PiholeDns(web_password=Config.from_secrets("pihole_webpwd"), api_url=Config.PIHOLE)
- success = pi.put_cname(service_fqdn, Config.INTERNAL_LOADBALANCER_HOST)
+ logger.info("Configuring internal DNS via LabDNS...")
+ dns = HomelabDns(
+ group_vars=Config.LABDNS_GROUP_VARS, delimiter=Config.LABDNS_DELIMITER
+ )
+ success = dns.put_internal(args.service_name)
if not success:
- logger.warning("Internal DNS (Pi-hole) CNAME creation failed.")
+ logger.warning("Internal DNS (LabDNS) Record creation failed.")
else:
- logger.info("Internal DNS (Pi-hole) CNAME created successfully.")
+ logger.info("Internal DNS (LabDNS) Record created successfully.")
logger.info("Generating Ansible role and configuration...")
- generator = RoleGenerator(args.service_name, args.container_image, args.service_port)
+ generator = RoleGenerator(
+ args.service_name, args.container_image, args.service_port
+ )
generator.create_all()
logger.info("Role generation complete.")
@@ -292,6 +313,6 @@ def main():
logger.info(f"Service '{args.service_name}' setup complete.")
+
if __name__ == "__main__":
main()
-