2021-10-20 08:54:41 +00:00
|
|
|
import functools
|
2021-10-26 10:19:02 +00:00
|
|
|
import io
|
2021-10-12 17:06:49 +00:00
|
|
|
from ipaddress import IPv4Network
|
|
|
|
from pathlib import Path
|
2021-10-26 10:19:02 +00:00
|
|
|
from typing import Optional, Dict, List, Any, TextIO
|
2020-08-04 18:24:19 +00:00
|
|
|
|
2021-10-12 17:08:35 +00:00
|
|
|
from pydantic import BaseModel, constr, root_validator, validator
|
2021-10-26 13:56:58 +00:00
|
|
|
from ruamel.yaml import YAML
|
2020-08-06 01:45:12 +00:00
|
|
|
|
2021-10-26 10:19:02 +00:00
|
|
|
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
|
|
|
|
from .misc import _format_kiwi_yml
|
2021-10-12 17:06:49 +00:00
|
|
|
|
2020-08-04 14:52:30 +00:00
|
|
|
|
2021-10-26 13:56:58 +00:00
|
|
|
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)}
|
|
|
|
|
2021-10-20 01:05:32 +00:00
|
|
|
@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
|
|
|
|
raise ValueError("Invalid Storage Format")
|
|
|
|
|
2020-08-13 08:48:01 +00:00
|
|
|
|
2021-10-26 13:56:58 +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
|
2021-10-26 13:56:58 +00:00
|
|
|
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"""
|
|
|
|
|
|
|
|
if self.override_storage is None:
|
|
|
|
return {self.name: self.enabled}
|
|
|
|
|
|
|
|
else:
|
|
|
|
result = self.dict(exclude={"override_storage"})
|
|
|
|
result["override_storage"] = self.override_storage.kiwi_dict
|
|
|
|
return result
|
|
|
|
|
2021-10-20 01:05:32 +00:00
|
|
|
@validator("override_storage", pre=True)
|
|
|
|
@classmethod
|
|
|
|
def unify_storage(cls, value):
|
2021-10-20 01:11:33 +00:00
|
|
|
"""parse different storage notations"""
|
|
|
|
|
2021-10-20 01:05:32 +00:00
|
|
|
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:
|
2021-10-26 10:19:02 +00:00
|
|
|
# undefined format
|
|
|
|
return {}
|
2021-10-20 01:05:32 +00:00
|
|
|
|
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
|
2021-10-20 01:05:32 +00:00
|
|
|
raise ValueError("Invalid Project Format")
|
2020-08-20 11:29:08 +00:00
|
|
|
|
|
|
|
|
2021-10-26 13:56:58 +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
|
|
|
|
2021-10-26 13:56:58 +00:00
|
|
|
class KiwiConfig(BaseModel):
|
2021-10-11 00:58:49 +00:00
|
|
|
"""represents a kiwi.yml"""
|
2020-08-08 17:41:11 +00:00
|
|
|
|
2021-10-12 17:17:28 +00:00
|
|
|
version: constr(regex=RE_SEMVER) = "0.2.0"
|
|
|
|
|
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
|
|
|
]
|
|
|
|
|
2021-10-26 13:56:58 +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]] = {}
|
|
|
|
|
2021-10-26 13:56:58 +00:00
|
|
|
storage: StorageConfig = StorageConfig(
|
2021-10-13 00:16:09 +00:00
|
|
|
directory="/var/local/kiwi",
|
|
|
|
)
|
2021-10-12 17:17:28 +00:00
|
|
|
|
2021-10-26 13:56:58 +00:00
|
|
|
network: NetworkConfig = NetworkConfig(
|
2021-10-13 00:16:09 +00:00
|
|
|
name="kiwi_hub",
|
|
|
|
cidr="10.22.46.0/24",
|
|
|
|
)
|
2020-08-08 17:41:11 +00:00
|
|
|
|
2021-10-20 08:54:41 +00:00
|
|
|
@classmethod
|
|
|
|
@functools.lru_cache(maxsize=5)
|
2021-10-26 13:56:58 +00:00
|
|
|
def from_directory(cls, directory: Path):
|
2021-10-20 12:32:45 +00:00
|
|
|
"""parses an actual kiwi.yml from disk (cached)"""
|
2021-10-20 08:54:41 +00:00
|
|
|
|
|
|
|
try:
|
2021-10-26 13:56:58 +00:00
|
|
|
with open(directory.joinpath(KIWI_CONF_NAME)) as kc:
|
|
|
|
return cls.parse_obj(YAML().load(kc))
|
2021-10-20 08:54:41 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
def from_default(cls):
|
|
|
|
"""returns the default config (cached)"""
|
|
|
|
|
|
|
|
return cls()
|
2021-10-20 08:54:41 +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"""
|
|
|
|
|
|
|
|
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-26 10:19:02 +00:00
|
|
|
def dump_kiwi_yml(self, stream: TextIO) -> None:
|
2021-10-13 01:05:46 +00:00
|
|
|
"""dump a kiwi.yml file"""
|
|
|
|
|
2021-10-26 13:56:58 +00:00
|
|
|
yml = YAML()
|
2021-10-26 10:19:02 +00:00
|
|
|
yml.indent(offset=2)
|
|
|
|
yml.dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
|
2021-10-13 01:05:46 +00:00
|
|
|
|
2021-10-26 10:19:02 +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-26 10:19:02 +00:00
|
|
|
sio = io.StringIO()
|
|
|
|
self.dump_kiwi_yml(sio)
|
|
|
|
result: str = sio.getvalue()
|
|
|
|
sio.close()
|
2021-10-13 01:05:46 +00:00
|
|
|
|
2021-10-26 10:19:02 +00:00
|
|
|
return result
|
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)]
|
|
|
|
|
2021-10-20 08:54:41 +00:00
|
|
|
except Exception:
|
2021-10-15 17:39:14 +00:00
|
|
|
# undefined format
|
|
|
|
raise ValueError("Invalid Shells Format")
|
|
|
|
|
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)})
|
|
|
|
|
2021-10-20 08:54:41 +00:00
|
|
|
except Exception:
|
2021-10-15 17:39:14 +00:00
|
|
|
# undefined format
|
|
|
|
raise ValueError("Invalid Projects Format")
|
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)}]
|
|
|
|
|
2021-10-20 08:54:41 +00:00
|
|
|
except Exception:
|
2021-10-15 17:39:14 +00:00
|
|
|
# undefined format
|
|
|
|
raise ValueError("Invalid Projects Format")
|
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-10-20 01:05:32 +00:00
|
|
|
def parse_str(var_val: Any) -> (str, Optional[str]):
|
2021-10-12 17:06:49 +00:00
|
|
|
"""parse a "<variable>=<value>" string"""
|
|
|
|
|
2021-10-20 01:05:32 +00:00
|
|
|
try:
|
|
|
|
idx = str(var_val).find("=")
|
|
|
|
except Exception:
|
|
|
|
# undefined format
|
|
|
|
raise ValueError("Invalid Environment Format")
|
|
|
|
|
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:
|
2021-10-20 01:05:32 +00:00
|
|
|
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>"
|
2021-10-20 01:05:32 +00:00
|
|
|
key, value = parse_str(value)
|
|
|
|
return {key: value}
|
|
|
|
|
|
|
|
@validator("storage", pre=True)
|
|
|
|
@classmethod
|
|
|
|
def unify_storage(cls, value):
|
|
|
|
"""parse different storage notations"""
|
|
|
|
|
|
|
|
if value is None:
|
2021-10-20 01:11:33 +00:00
|
|
|
# empty storage
|
2021-10-20 01:05:32 +00:00
|
|
|
raise ValueError("No Storage Given")
|
|
|
|
|
|
|
|
elif isinstance(value, dict):
|
2021-10-20 01:11:33 +00:00
|
|
|
# native dict format
|
2021-10-20 01:05:32 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
elif isinstance(value, str):
|
2021-10-20 01:11:33 +00:00
|
|
|
# just the directory string
|
2021-10-20 01:05:32 +00:00
|
|
|
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
|
2021-10-20 01:05:32 +00:00
|
|
|
return {"directory": value[0]}
|
|
|
|
|
|
|
|
else:
|
2021-10-20 01:11:33 +00:00
|
|
|
# undefined format
|
2021-10-26 10:19:02 +00:00
|
|
|
return {}
|
2021-10-20 01:05:32 +00:00
|
|
|
|
|
|
|
@validator("network", pre=True)
|
|
|
|
@classmethod
|
|
|
|
def unify_network(cls, value):
|
|
|
|
"""parse different network notations"""
|
|
|
|
|
|
|
|
if value is None:
|
2021-10-20 01:11:33 +00:00
|
|
|
# empty network
|
2021-10-20 01:05:32 +00:00
|
|
|
raise ValueError("No Network Given")
|
|
|
|
|
|
|
|
elif isinstance(value, dict):
|
2021-10-20 01:11:33 +00:00
|
|
|
# native dict format
|
2021-10-20 01:05:32 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
else:
|
2021-10-20 01:11:33 +00:00
|
|
|
# undefined format
|
2021-10-20 01:05:32 +00:00
|
|
|
raise ValueError("Invalid Network Format")
|