diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-05-03 20:52:54 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-05-03 21:29:25 -0700 |
commit | 7106b304324b3a7c4dc5fa25432f08041cbc41cb (patch) | |
tree | cac71c9d85ba0045a735a4709573634c52f38e03 /create.py | |
parent | ae2d222ab5423bef0fc136e0aee2b4243db03b17 (diff) | |
download | infra-7106b304324b3a7c4dc5fa25432f08041cbc41cb.tar.gz infra-7106b304324b3a7c4dc5fa25432f08041cbc41cb.zip |
Move to unbound
Diffstat (limited to 'create.py')
-rwxr-xr-x | create.py | 151 |
1 files changed, 86 insertions, 65 deletions
@@ -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() - |