Add head-only recipes, plus start one to do Hurricane DNS

This commit is contained in:
Olivier 'reivilibre' 2022-03-16 12:54:42 +00:00
parent b0fbdb2d1e
commit 6e16ac7ec6
6 changed files with 165 additions and 3 deletions

View File

@ -0,0 +1,126 @@
# Copyright 2022, Olivier 'reivilibre'.
#
# This file is part of Scone.
#
# Scone is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Scone is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scone. If not, see <https://www.gnu.org/licenses/>.
from asyncio import Lock
from typing import Any, Dict, List, Optional, Tuple, Union
import attr
from HurricaneDNS import HurricaneDNS
from scone.head.head import Head
from scone.head.kitchen import Kitchen, Preparation
from scone.head.recipe import HeadRecipe, RecipeContext
from scone.head.utils import check_type
@attr.s(auto_attribs=True)
class DnsRecord:
pass
def parse_records(given: Union[Any, Dict[str, Any]]) -> List[DnsRecord]:
pass
@attr.s(auto_attribs=True)
class HeRecord:
id: str
status: Optional[str]
host: str
type: str
ttl: str
# MX Priority
mx: str
value: str
extended: str
@attr.s(auto_attribs=True)
class HeDomain:
domain: str
id: str
type: str
records: Optional[List[HeRecord]]
@attr.s(auto_attribs=True)
class HurricaneElectricCache:
client: HurricaneDNS
lock: Lock
domains: Dict[str, HeDomain]
# Tuple from (id(head), user, password) → HE DNS cache)
# If Scone is ever long-living, this could leak, but it doesn't so it won't matter
HE_CLIENT_CACHE: Dict[Tuple[int, str, str], HurricaneElectricCache] = {}
CLIENT_CACHE_LOCK: Lock = Lock()
class HurricaneElectricDns(HeadRecipe):
_NAME = "dns-hurricane"
def __init__(self, recipe_context: RecipeContext, args: dict, head: Head):
super().__init__(recipe_context, args, head)
self.username = check_type(args.get("username"), str)
self.password = check_type(args.get("password"), str)
self.domain = check_type(args.get("domain"), str)
self.records = parse_records(args.get("records"))
def prepare(self, preparation: Preparation, head: Head) -> None:
super().prepare(preparation, head)
async def _get_client(self, head: Head) -> HurricaneElectricCache:
async with CLIENT_CACHE_LOCK:
cache_key = id(head), self.username, self.password
if cache_key in HE_CLIENT_CACHE:
# Happy days
return HE_CLIENT_CACHE[cache_key]
# TODO(performance): this takes about 3 sec; move it to an executor thread
client = HurricaneDNS(self.username, self.password)
domains = {}
for domain in client.list_domains():
dom = HeDomain(**domain)
domains[dom.domain] = dom
entry = HurricaneElectricCache(client, Lock(), domains)
HE_CLIENT_CACHE[cache_key] = entry
return entry
async def _get_records(
self, cached: HurricaneElectricCache, domain: HeDomain
) -> List[HeRecord]:
async with cached.lock:
if domain.records is not None:
return domain.records
domain.records = [
HeRecord(**row) for row in cached.client.list_records(domain.domain)
]
return domain.records
async def cook(self, kitchen: Kitchen) -> None:
kitchen.get_dependency_tracker().ignore()
cached = await self._get_client(kitchen.head)
records = await self._get_records(cached, cached.domains[self.domain])
for record in records:
print(record)

View File

@ -92,6 +92,7 @@ async def cli_async() -> int:
eprint(f"Selected the following souss: {', '.join(hosts)}")
# Load variables for the head as well.
head.load_variables(hosts)
head.load_menus(menu_subset, hosts)

View File

@ -37,6 +37,8 @@ from scone.head.variables import Variables, merge_right_into_left_inplace
logger = logging.getLogger(__name__)
SPECIAL_HEAD_SOUS = "head"
class Head:
def __init__(
@ -52,6 +54,8 @@ class Head:
self.recipe_loader = recipe_loader
self.dag = RecipeDag()
self.souss = sous
# Special override: head sous.
self.souss[SPECIAL_HEAD_SOUS] = {"user": ""}
self.groups = groups
self.secret_access = secret_access
self.variables: Dict[str, Variables] = dict()
@ -79,7 +83,8 @@ class Head:
sous = head_data.get("sous", dict())
groups = head_data.get("group", dict())
groups["all"] = list(sous.keys())
groups["all_plus_head"] = list(sous.keys())
groups["all"] = list(sous.keys() - "head")
pools = Pools()
@ -91,6 +96,10 @@ class Head:
out_chilled: Dict[str, Any] = {}
vardir = Path(self.directory, "vars", who_for)
# TODO(feature): is this needed?
# if not vardir.exists():
# return out_chilled, out_frozen
logger.debug("preloading vars for %s in %s", who_for, str(vardir))
for file in vardir.glob("*.vf.toml"):

View File

@ -40,7 +40,7 @@ from scone.head.dependency_tracking import (
DependencyTracker,
hash_dict,
)
from scone.head.head import Head
from scone.head.head import SPECIAL_HEAD_SOUS, Head
from scone.head.recipe import Recipe
from scone.sous import utensil_namer
from scone.sous.utensils import Utensil
@ -167,6 +167,9 @@ class Kitchen:
return ChanProHead(cp, root)
if host == SPECIAL_HEAD_SOUS:
raise ValueError("Can't connect to special 'head' sous over SSH")
hostuser = (host, user)
if hostuser not in self._chanproheads:
self._chanproheads[hostuser] = asyncio.create_task(new_conn())

View File

@ -81,3 +81,20 @@ class Recipe:
f"{cls.__name__} {self.recipe_context.human}"
f" on {self.recipe_context.sous} ({self.arguments})"
)
class HeadRecipe(Recipe):
def __init__(
self, recipe_context: RecipeContext, args: Dict[str, Any], head: "Head"
):
super().__init__(recipe_context, args, head)
if recipe_context.sous != "head":
myname = self.__class__._NAME
raise ValueError(
f"[[{myname}]] is a head recipe, so should be run with @sous = head"
)
def prepare(self, preparation: "Preparation", head: "Head") -> None:
# Don't add a requirement for an os-user, since it's a head recipe
# that doesn't have one.
pass

View File

@ -54,12 +54,18 @@ EX_HEAD = [
"typeguard",
"textx",
]
EX_HEAD_HEDNS = [
"hurricanedns~=1.0.2",
]
EX_HEAD_ALL = EX_HEAD + EX_HEAD_HEDNS
EX_DEV = EX_SOUS_ALL + EX_HEAD + EX_DEV_MYPY
# What packages are optional?
EXTRAS = {
"head": EX_HEAD,
"head-core": EX_HEAD,
"head": EX_HEAD_ALL,
"head-hedns": EX_HEAD_HEDNS,
"sous": EX_SOUS_ALL,
"sous-core": EX_SOUS_BASE,
"sous-pg": EX_SOUS_PG,