Add head-only recipes, plus start one to do Hurricane DNS
This commit is contained in:
parent
b0fbdb2d1e
commit
6e16ac7ec6
126
scone/default/recipes/dns_he.py
Normal file
126
scone/default/recipes/dns_he.py
Normal 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)
|
@ -92,6 +92,7 @@ async def cli_async() -> int:
|
|||||||
|
|
||||||
eprint(f"Selected the following souss: {', '.join(hosts)}")
|
eprint(f"Selected the following souss: {', '.join(hosts)}")
|
||||||
|
|
||||||
|
# Load variables for the head as well.
|
||||||
head.load_variables(hosts)
|
head.load_variables(hosts)
|
||||||
head.load_menus(menu_subset, hosts)
|
head.load_menus(menu_subset, hosts)
|
||||||
|
|
||||||
|
@ -37,6 +37,8 @@ from scone.head.variables import Variables, merge_right_into_left_inplace
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SPECIAL_HEAD_SOUS = "head"
|
||||||
|
|
||||||
|
|
||||||
class Head:
|
class Head:
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -52,6 +54,8 @@ class Head:
|
|||||||
self.recipe_loader = recipe_loader
|
self.recipe_loader = recipe_loader
|
||||||
self.dag = RecipeDag()
|
self.dag = RecipeDag()
|
||||||
self.souss = sous
|
self.souss = sous
|
||||||
|
# Special override: head sous.
|
||||||
|
self.souss[SPECIAL_HEAD_SOUS] = {"user": ""}
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
self.secret_access = secret_access
|
self.secret_access = secret_access
|
||||||
self.variables: Dict[str, Variables] = dict()
|
self.variables: Dict[str, Variables] = dict()
|
||||||
@ -79,7 +83,8 @@ class Head:
|
|||||||
|
|
||||||
sous = head_data.get("sous", dict())
|
sous = head_data.get("sous", dict())
|
||||||
groups = head_data.get("group", 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()
|
pools = Pools()
|
||||||
|
|
||||||
@ -91,6 +96,10 @@ class Head:
|
|||||||
out_chilled: Dict[str, Any] = {}
|
out_chilled: Dict[str, Any] = {}
|
||||||
vardir = Path(self.directory, "vars", who_for)
|
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))
|
logger.debug("preloading vars for %s in %s", who_for, str(vardir))
|
||||||
|
|
||||||
for file in vardir.glob("*.vf.toml"):
|
for file in vardir.glob("*.vf.toml"):
|
||||||
|
@ -40,7 +40,7 @@ from scone.head.dependency_tracking import (
|
|||||||
DependencyTracker,
|
DependencyTracker,
|
||||||
hash_dict,
|
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.head.recipe import Recipe
|
||||||
from scone.sous import utensil_namer
|
from scone.sous import utensil_namer
|
||||||
from scone.sous.utensils import Utensil
|
from scone.sous.utensils import Utensil
|
||||||
@ -167,6 +167,9 @@ class Kitchen:
|
|||||||
|
|
||||||
return ChanProHead(cp, root)
|
return ChanProHead(cp, root)
|
||||||
|
|
||||||
|
if host == SPECIAL_HEAD_SOUS:
|
||||||
|
raise ValueError("Can't connect to special 'head' sous over SSH")
|
||||||
|
|
||||||
hostuser = (host, user)
|
hostuser = (host, user)
|
||||||
if hostuser not in self._chanproheads:
|
if hostuser not in self._chanproheads:
|
||||||
self._chanproheads[hostuser] = asyncio.create_task(new_conn())
|
self._chanproheads[hostuser] = asyncio.create_task(new_conn())
|
||||||
|
@ -81,3 +81,20 @@ class Recipe:
|
|||||||
f"{cls.__name__} {self.recipe_context.human}"
|
f"{cls.__name__} {self.recipe_context.human}"
|
||||||
f" on {self.recipe_context.sous} ({self.arguments})"
|
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
|
||||||
|
8
setup.py
8
setup.py
@ -54,12 +54,18 @@ EX_HEAD = [
|
|||||||
"typeguard",
|
"typeguard",
|
||||||
"textx",
|
"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
|
EX_DEV = EX_SOUS_ALL + EX_HEAD + EX_DEV_MYPY
|
||||||
|
|
||||||
# What packages are optional?
|
# What packages are optional?
|
||||||
EXTRAS = {
|
EXTRAS = {
|
||||||
"head": EX_HEAD,
|
"head-core": EX_HEAD,
|
||||||
|
"head": EX_HEAD_ALL,
|
||||||
|
"head-hedns": EX_HEAD_HEDNS,
|
||||||
"sous": EX_SOUS_ALL,
|
"sous": EX_SOUS_ALL,
|
||||||
"sous-core": EX_SOUS_BASE,
|
"sous-core": EX_SOUS_BASE,
|
||||||
"sous-pg": EX_SOUS_PG,
|
"sous-pg": EX_SOUS_PG,
|
||||||
|
Loading…
Reference in New Issue
Block a user