1
0
Fork 0
mirror of https://github.com/yavook/kiwi-scp.git synced 2024-11-24 05:33:00 +00:00
kiwi-scp/kiwi_scp/config.py

403 lines
11 KiB
Python
Raw Permalink Normal View History

import functools
2021-10-12 17:06:49 +00:00
from ipaddress import IPv4Network
from pathlib import Path
2021-11-27 15:21:54 +00:00
from typing import Optional, Dict, List, Any, TextIO, Tuple
2020-08-04 18:24:19 +00:00
2021-10-12 17:08:35 +00:00
from pydantic import BaseModel, constr, root_validator, validator
2020-08-06 01:45:12 +00:00
2022-01-24 15:53:19 +00:00
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME, RESERVED_PROJECT_NAMES
2021-12-02 16:06:13 +00:00
from .yaml import YAML
2021-10-12 17:06:49 +00:00
2020-08-04 14:52:30 +00:00
2022-02-22 12:18:35 +00:00
class InvalidFormatError(ValueError):
"""raised if format recognition unsuccessful"""
cls: type
member: Optional[str]
data: str
def __init__(self, cls, data, member = None):
self.cls = cls
self.data = data
if member is not None:
self.member = member
super().__init__(f"Invalid {self.cls.__name__!r}.{self.member!r} Format: {self.data!r}")
else:
super().__init__(f"Invalid {self.cls.__name__!r} Format: {self.data!r}")
class StorageConfig(BaseModel):
2021-10-11 00:58:49 +00:00
"""a storage subsection"""
2020-08-13 08:48:01 +00:00
2021-10-12 17:06:49 +00:00
directory: Path
2020-08-06 01:45:12 +00:00
2021-10-13 01:05:46 +00:00
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
return {"directory": str(self.directory)}
@root_validator(pre=True)
@classmethod
def unify_storage(cls, values) -> Dict[str, Any]:
"""parse different storage notations"""
if "directory" in values:
# default format
return values
else:
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(cls, str(values))
class ProjectNameReservedError(ValueError):
"""raised if trying to create a project with a reserved name"""
name: str
def __init__(self, name):
self.name = name
super().__init__(f"Project name {self.name!r} is reserved!")
2020-08-13 08:48:01 +00:00
class ProjectConfig(BaseModel):
2021-10-11 00:58:49 +00:00
"""a project subsection"""
2020-08-06 12:34:25 +00:00
2021-10-13 01:05:46 +00:00
name: constr(regex=RE_VARNAME)
2021-10-11 00:58:49 +00:00
enabled: bool = True
override_storage: Optional[StorageConfig]
2020-08-06 12:34:25 +00:00
2021-10-13 01:05:46 +00:00
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
2022-02-21 21:46:50 +00:00
result = self.dict(exclude={"override_storage"})
2021-10-13 01:05:46 +00:00
2022-02-21 21:46:50 +00:00
if self.override_storage is not None:
2021-10-13 01:05:46 +00:00
result["override_storage"] = self.override_storage.kiwi_dict
2022-02-21 21:46:50 +00:00
return result
2021-10-13 01:05:46 +00:00
2022-01-24 15:53:19 +00:00
@validator("name")
@classmethod
def check_project(cls, value: str) -> str:
"""check if project name is allowed"""
if value in RESERVED_PROJECT_NAMES:
2022-02-22 12:18:35 +00:00
raise ProjectNameReservedError(value)
2022-01-24 15:53:19 +00:00
return value
@validator("override_storage", pre=True)
@classmethod
2021-11-27 15:21:54 +00:00
def unify_storage(cls, value) -> Dict[str, Any]:
2021-10-20 01:11:33 +00:00
"""parse different storage notations"""
if value is None or isinstance(value, dict):
return value
elif isinstance(value, str):
return {"directory": value}
elif isinstance(value, list) and len(value) == 1:
return {"directory": value[0]}
else:
# undefined format
return {}
2021-10-12 17:08:35 +00:00
@root_validator(pre=True)
2021-10-11 00:58:49 +00:00
@classmethod
2021-10-15 17:39:14 +00:00
def unify_project(cls, values) -> Dict[str, Any]:
2021-10-12 17:06:49 +00:00
"""parse different project notations"""
if "name" in values:
# default format
return values
elif len(values) == 1:
# short format:
# - <name>: <enabled>
2020-08-11 15:23:24 +00:00
2021-10-12 17:06:49 +00:00
name, enabled = list(values.items())[0]
return {
"name": name,
2021-10-14 03:12:33 +00:00
"enabled": True if enabled is None else enabled,
2021-10-12 17:06:49 +00:00
}
2020-08-11 15:23:24 +00:00
2021-10-12 17:06:49 +00:00
else:
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(ProjectConfig, values)
2020-08-20 11:29:08 +00:00
class NetworkConfig(BaseModel):
2021-10-11 00:58:49 +00:00
"""a network subsection"""
2020-08-20 11:29:08 +00:00
2021-10-12 17:17:28 +00:00
name: constr(to_lower=True, regex=RE_VARNAME)
2021-10-12 17:06:49 +00:00
cidr: IPv4Network
2020-08-13 08:48:01 +00:00
2021-10-13 01:05:46 +00:00
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
return {
"name": self.name,
2021-10-14 03:12:33 +00:00
"cidr": str(self.cidr),
2021-10-13 01:05:46 +00:00
}
2020-08-06 12:34:25 +00:00
2022-02-22 12:18:35 +00:00
class MissingMemberError(ValueError):
"""raised if class member is missing a definition"""
cls: type
member: str
def __init__(self, cls, member):
self.cls = cls
self.member = member
super().__init__(f"Member {self.cls.__name__!r}.{self.member!r} is required!")
class KiwiConfig(BaseModel):
2021-10-11 00:58:49 +00:00
"""represents a kiwi.yml"""
2022-02-22 12:55:47 +00:00
version: constr(regex=RE_SEMVER) = "0.2.0"
2021-10-12 17:17:28 +00:00
2021-10-15 17:39:14 +00:00
shells: List[Path] = [
2021-10-13 00:16:09 +00:00
Path("/bin/bash"),
2021-10-12 17:17:28 +00:00
]
projects: List[ProjectConfig] = []
2021-10-12 17:17:28 +00:00
2021-10-13 01:05:46 +00:00
environment: Dict[str, Optional[str]] = {}
storage: StorageConfig = StorageConfig(
2021-10-13 00:16:09 +00:00
directory="/var/local/kiwi",
)
2021-10-12 17:17:28 +00:00
network: NetworkConfig = NetworkConfig(
2021-10-13 00:16:09 +00:00
name="kiwi_hub",
cidr="10.22.46.0/24",
)
@classmethod
@functools.lru_cache(maxsize=5)
2021-11-27 15:21:54 +00:00
def from_directory(cls, directory: Path) -> "KiwiConfig":
2021-10-20 12:32:45 +00:00
"""parses an actual kiwi.yml from disk (cached)"""
try:
with open(directory.joinpath(KIWI_CONF_NAME)) as kc:
return cls.parse_obj(YAML().load(kc))
except FileNotFoundError:
# return the defaults if no kiwi.yml found
2021-10-20 12:32:45 +00:00
return cls.from_default()
@classmethod
@functools.lru_cache(maxsize=1)
2021-11-27 15:21:54 +00:00
def from_default(cls) -> "KiwiConfig":
2021-10-20 12:32:45 +00:00
"""returns the default config (cached)"""
return cls()
2021-10-28 13:39:11 +00:00
def get_project_config(self, name: str) -> Optional[ProjectConfig]:
2021-10-26 13:59:38 +00:00
"""returns the config of a project with a given name"""
for project in self.projects:
if project.name == name:
return project
2021-10-13 01:05:46 +00:00
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
result = {
"version": self.version,
"shells": [str(shell) for shell in self.shells],
}
if self.projects:
result["projects"] = [
project.kiwi_dict
for project in self.projects
]
if self.environment:
result["environment"] = self.environment
result["storage"] = self.storage.kiwi_dict
result["network"] = self.network.kiwi_dict
return result
2021-10-28 11:54:02 +00:00
def dump_kiwi_yml(self, stream: TextIO = None) -> Optional[str]:
2021-10-13 01:05:46 +00:00
"""dump a kiwi.yml file"""
2021-10-28 11:54:02 +00:00
return YAML().dump_kiwi_yml(self.kiwi_dict, stream=stream)
2021-10-13 01:05:46 +00:00
@property
def kiwi_yml(self) -> str:
"""get a kiwi.yml dump as a string"""
2021-10-13 01:05:46 +00:00
2021-10-28 11:54:02 +00:00
return self.dump_kiwi_yml()
2021-10-13 01:05:46 +00:00
2021-10-15 17:39:14 +00:00
@validator("shells", pre=True)
@classmethod
def unify_shells(cls, value) -> List[str]:
"""parse different shells notations"""
if value is None:
return []
elif isinstance(value, list):
return value
elif isinstance(value, dict):
return list(value)
else:
# any other format (try to coerce to str first)
try:
return [str(value)]
except Exception:
2021-10-15 17:39:14 +00:00
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(KiwiConfig, value, "shells")
2021-10-15 17:39:14 +00:00
2021-10-14 17:18:24 +00:00
@validator("projects", pre=True)
@classmethod
2021-10-15 17:39:14 +00:00
def unify_projects(cls, value) -> List[Dict[str, str]]:
2021-10-14 17:18:24 +00:00
"""parse different projects notations"""
if value is None:
# empty projects list
return []
elif isinstance(value, list):
# handle projects list
result = []
for entry in value:
# ignore empties
if entry is not None:
if isinstance(entry, dict):
# handle single project dict
result.append(entry)
else:
2021-10-15 17:39:14 +00:00
try:
# handle single project name
result.append({"name": str(entry)})
except Exception:
2021-10-15 17:39:14 +00:00
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(KiwiConfig, value, "projects")
2021-10-14 17:18:24 +00:00
return result
elif isinstance(value, dict):
# handle single project dict
return [value]
else:
2021-10-15 17:39:14 +00:00
# any other format (try to coerce to str first)
try:
# handle as a single project name
return [{"name": str(value)}]
except Exception:
2021-10-15 17:39:14 +00:00
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(KiwiConfig, value, "projects")
2021-10-14 17:18:24 +00:00
2021-10-12 17:08:35 +00:00
@validator("environment", pre=True)
2020-08-06 11:43:45 +00:00
@classmethod
2021-10-12 17:17:28 +00:00
def unify_environment(cls, value) -> Dict[str, Optional[str]]:
2021-10-12 17:06:49 +00:00
"""parse different environment notations"""
2021-11-27 15:21:54 +00:00
def parse_str(var_val: Any) -> Tuple[str, Optional[str]]:
2021-10-12 17:06:49 +00:00
"""parse a "<variable>=<value>" string"""
try:
idx = str(var_val).find("=")
except Exception:
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(KiwiConfig, value, "environment")
2021-10-12 17:06:49 +00:00
if idx == -1:
# don't split, just define the variable
return var_val, None
else:
# split string, set variable to value
return var_val[:idx], var_val[idx + 1:]
if value is None:
# empty environment
2021-10-12 17:17:28 +00:00
return {}
2021-10-12 17:06:49 +00:00
elif isinstance(value, dict):
# native dict format
2021-10-11 00:58:49 +00:00
return value
2021-10-12 17:06:49 +00:00
2021-10-11 00:58:49 +00:00
elif isinstance(value, list):
2021-10-12 17:06:49 +00:00
# list format (multiple strings)
2021-10-11 00:58:49 +00:00
result: Dict[str, Optional[str]] = {}
for item in value:
key, value = parse_str(item)
result[key] = value
2021-10-12 17:06:49 +00:00
2021-10-15 17:39:14 +00:00
return result
2021-10-12 17:06:49 +00:00
2021-10-11 00:58:49 +00:00
else:
2021-10-14 02:34:09 +00:00
# any other format (try to coerce to str first)
2021-10-15 17:39:14 +00:00
# string format (single variable):
# "<var>=<value>"
key, value = parse_str(value)
return {key: value}
@validator("storage", pre=True)
@classmethod
2021-11-27 15:21:54 +00:00
def unify_storage(cls, value) -> Dict[str, Any]:
"""parse different storage notations"""
if value is None:
2021-10-20 01:11:33 +00:00
# empty storage
2022-02-22 12:18:35 +00:00
raise MissingMemberError(KiwiConfig, "storage")
elif isinstance(value, dict):
2021-10-20 01:11:33 +00:00
# native dict format
return value
elif isinstance(value, str):
2021-10-20 01:11:33 +00:00
# just the directory string
return {"directory": value}
2021-10-20 01:11:33 +00:00
elif isinstance(value, list) and len(value) == 1 and isinstance(value[0], str):
# directory string as a single-item list
return {"directory": value[0]}
else:
2021-10-20 01:11:33 +00:00
# undefined format
return {}
@validator("network", pre=True)
@classmethod
2021-11-27 15:21:54 +00:00
def unify_network(cls, value) -> Dict[str, Any]:
"""parse different network notations"""
if value is None:
2021-10-20 01:11:33 +00:00
# empty network
2022-02-22 12:18:35 +00:00
raise MissingMemberError(KiwiConfig, "network")
elif isinstance(value, dict):
2021-10-20 01:11:33 +00:00
# native dict format
return value
else:
2021-10-20 01:11:33 +00:00
# undefined format
2022-02-22 12:18:35 +00:00
raise InvalidFormatError(KiwiConfig, value, "network")