From 7360e0d614964b1c951f08d8427f4edfe5d93e88 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Thu, 17 Mar 2022 23:18:33 +0000 Subject: [PATCH] Support adding HE DNS entries --- scone/default/recipes/dns_he.py | 90 +++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/scone/default/recipes/dns_he.py b/scone/default/recipes/dns_he.py index b446fc1..020cf7e 100644 --- a/scone/default/recipes/dns_he.py +++ b/scone/default/recipes/dns_he.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with Scone. If not, see . +import logging from asyncio import Lock from typing import Any, Dict, List, Optional, Tuple, Union @@ -25,14 +26,62 @@ from scone.head.kitchen import Kitchen, Preparation from scone.head.recipe import HeadRecipe, RecipeContext from scone.head.utils import check_type +logger = logging.getLogger(__name__) + @attr.s(auto_attribs=True) class DnsRecord: - pass + subdomain: str + kind: str + value: str + ttl: Optional[str] + priority: Optional[str] -def parse_records(given: Union[Any, Dict[str, Any]]) -> List[DnsRecord]: - pass +def parse_records(given_raw: Union[Any, Dict[str, Dict[str, str]]]) -> List[DnsRecord]: + given = check_type(given_raw, dict, "records") + + the_list: List[DnsRecord] = [] + + for key, attributes in given.items(): + # keys: "xyz A" + # values: dicts, with keys: + # - v: 1.2.3.4 + # - ttl: 86400 + # - priority: 50 (for MXes) + + pieces = key.split(" ") + if len(pieces) > 2: + raise ValueError( + f"Key {key} should be space-separable with 2 or less pieces." + ) + + if len(pieces) == 2: + subdomain, kind = pieces + else: + assert len(pieces) == 1 + (kind,) = pieces + subdomain = "" + + ttl_raw = attributes.get("ttl") + prio_raw = attributes.get("priority") + + record_value = attributes.get("v", attributes.get("value", None)) + + if record_value is None: + raise ValueError("No record value") + + the_list.append( + DnsRecord( + subdomain=subdomain, + kind=kind, + value=record_value, + ttl=None if ttl_raw is None else str(ttl_raw), + priority=None if prio_raw is None else str(prio_raw), + ) + ) + + return the_list @attr.s(auto_attribs=True) @@ -117,10 +166,43 @@ class HurricaneElectricDns(HeadRecipe): return domain.records async def cook(self, kitchen: Kitchen) -> None: + # TODO(correctness): can't handle multiple DNS records + # with same (type, subdomain) kitchen.get_dependency_tracker().ignore() cached = await self._get_client(kitchen.head) records = await self._get_records(cached, cached.domains[self.domain]) + records_cache: Dict[Tuple[str, str], HeRecord] = {} + for record in records: - print(record) + dotted_subdomain_suffix = f".{self.domain}" + if record.host == self.domain: + subdomain = "" + elif record.host.endswith(dotted_subdomain_suffix): + subdomain = record.host[: -len(dotted_subdomain_suffix)] + else: + raise ValueError(f"Can't figure out subdomain for {record.host}") + + records_cache[(subdomain, record.type)] = record + + logger.debug("Present records: %r", records_cache.keys()) + + for wanted_record in self.records: + wr_key = (wanted_record.subdomain, wanted_record.kind) + logger.debug("Want %r: %r", wr_key, wanted_record) + existing_record = records_cache.get(wr_key) + if existing_record is not None: + # TODO(correctness): amend as needed + logger.debug("Found existing %r", existing_record) + else: + logger.debug("Will need to create new one") + async with cached.lock: + cached.client.add_record( + self.domain, + wanted_record.subdomain, + wanted_record.kind, + wanted_record.value, + wanted_record.priority or None, + wanted_record.ttl or 86400, + )