2022-08-18 03:08:34 +02:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from copy import deepcopy
|
2022-11-09 15:22:06 +01:00
|
|
|
from typing import Optional
|
2022-08-18 03:08:34 +02:00
|
|
|
|
2022-08-19 20:57:51 +02:00
|
|
|
from .scheme import Profile, SparseProfile
|
2022-08-18 03:08:34 +02:00
|
|
|
|
2022-08-19 20:57:51 +02:00
|
|
|
PROFILE_DEFAULTS_DICT = {
|
2022-08-18 03:08:34 +02:00
|
|
|
'parent': '',
|
|
|
|
'device': '',
|
|
|
|
'flavour': '',
|
|
|
|
'pkgs_include': [],
|
|
|
|
'pkgs_exclude': [],
|
|
|
|
'hostname': 'kupfer',
|
|
|
|
'username': 'kupfer',
|
|
|
|
'password': None,
|
|
|
|
'size_extra_mb': "0",
|
2022-08-19 20:57:51 +02:00
|
|
|
}
|
|
|
|
PROFILE_DEFAULTS = Profile.fromDict(PROFILE_DEFAULTS_DICT)
|
2022-08-18 03:08:34 +02:00
|
|
|
|
|
|
|
PROFILE_EMPTY: Profile = {key: None for key in PROFILE_DEFAULTS.keys()} # type: ignore
|
|
|
|
|
|
|
|
|
2023-06-25 03:45:26 +02:00
|
|
|
class ProfileNotFoundException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2022-08-18 03:08:34 +02:00
|
|
|
def resolve_profile(
|
|
|
|
name: str,
|
2022-08-19 20:57:51 +02:00
|
|
|
sparse_profiles: dict[str, SparseProfile],
|
2022-11-09 15:22:06 +01:00
|
|
|
resolved: Optional[dict[str, Profile]] = None,
|
2022-08-18 03:08:34 +02:00
|
|
|
_visited=None,
|
|
|
|
) -> dict[str, Profile]:
|
|
|
|
"""
|
|
|
|
Recursively resolves the specified profile by `name` and its parents to merge the config semantically,
|
|
|
|
applying include and exclude overrides along the hierarchy.
|
|
|
|
If `resolved` is passed `None`, a fresh dictionary will be created.
|
|
|
|
`resolved` will be modified in-place during parsing and also returned.
|
|
|
|
A sanitized `sparse_profiles` dict is assumed, no checking for unknown keys or incorrect data types is performed.
|
|
|
|
`_visited` should not be passed by users.
|
|
|
|
"""
|
|
|
|
if _visited is None:
|
|
|
|
_visited = list[str]()
|
|
|
|
if resolved is None:
|
|
|
|
resolved = dict[str, Profile]()
|
|
|
|
if name in _visited:
|
|
|
|
loop = list(_visited)
|
|
|
|
raise Exception(f'Dependency loop detected in profiles: {" -> ".join(loop+[loop[0]])}')
|
|
|
|
if name in resolved:
|
|
|
|
return resolved
|
|
|
|
|
|
|
|
logging.debug(f'Resolving profile {name}')
|
|
|
|
_visited.append(name)
|
2022-08-19 20:57:51 +02:00
|
|
|
sparse = sparse_profiles[name].copy()
|
2022-08-18 03:08:34 +02:00
|
|
|
full = deepcopy(sparse)
|
2022-08-19 20:57:51 +02:00
|
|
|
if name != 'default' and 'parent' not in sparse:
|
|
|
|
sparse['parent'] = 'default'
|
2022-08-18 03:08:34 +02:00
|
|
|
if 'parent' in sparse and (parent_name := sparse['parent']):
|
|
|
|
parent = resolve_profile(name=parent_name, sparse_profiles=sparse_profiles, resolved=resolved, _visited=_visited)[parent_name]
|
|
|
|
full = parent | sparse
|
|
|
|
# add up size_extra_mb
|
|
|
|
if 'size_extra_mb' in sparse:
|
|
|
|
size = sparse['size_extra_mb']
|
|
|
|
if isinstance(size, str) and size.startswith('+'):
|
|
|
|
full['size_extra_mb'] = int(parent.get('size_extra_mb', 0)) + int(size.lstrip('+'))
|
|
|
|
else:
|
|
|
|
full['size_extra_mb'] = int(sparse['size_extra_mb'])
|
|
|
|
# join our includes with parent's
|
|
|
|
includes = set(parent.get('pkgs_include', []) + sparse.get('pkgs_include', []))
|
|
|
|
if 'pkgs_exclude' in sparse:
|
|
|
|
includes -= set(sparse['pkgs_exclude'])
|
|
|
|
full['pkgs_include'] = list(includes)
|
|
|
|
|
|
|
|
# join our includes with parent's
|
|
|
|
excludes = set(parent.get('pkgs_exclude', []) + sparse.get('pkgs_exclude', []))
|
|
|
|
# our includes override parent excludes
|
|
|
|
if 'pkgs_include' in sparse:
|
|
|
|
excludes -= set(sparse['pkgs_include'])
|
|
|
|
full['pkgs_exclude'] = list(excludes)
|
|
|
|
|
|
|
|
# now init missing keys
|
2022-08-19 20:57:51 +02:00
|
|
|
for key, value in PROFILE_DEFAULTS_DICT.items():
|
2022-08-18 03:08:34 +02:00
|
|
|
if key not in full.keys():
|
2022-08-19 20:57:51 +02:00
|
|
|
full[key] = value # type: ignore[literal-required]
|
2022-08-18 03:08:34 +02:00
|
|
|
if type(value) == list:
|
|
|
|
full[key] = [] # type: ignore[literal-required]
|
|
|
|
|
|
|
|
full['size_extra_mb'] = int(full['size_extra_mb'] or 0)
|
|
|
|
|
2022-08-19 20:57:51 +02:00
|
|
|
resolved[name] = Profile.fromDict(full)
|
2022-08-18 03:08:34 +02:00
|
|
|
return resolved
|
2023-06-25 03:45:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
def resolve_profile_attr(
|
|
|
|
profile_name: str,
|
|
|
|
attr_name: str,
|
|
|
|
profiles_sparse: dict[str, SparseProfile],
|
|
|
|
) -> tuple[str, str]:
|
|
|
|
"""
|
|
|
|
This function tries to resolve a profile attribute recursively,
|
|
|
|
and throws KeyError if the key is not found anywhere in the hierarchy.
|
|
|
|
Throws a ProfileNotFoundException if the profile is not in profiles_sparse
|
|
|
|
"""
|
|
|
|
if profile_name not in profiles_sparse:
|
|
|
|
raise ProfileNotFoundException(f"Unknown profile {profile_name}")
|
|
|
|
profile: Profile = profiles_sparse[profile_name]
|
|
|
|
if attr_name in profile:
|
|
|
|
return profile[attr_name], profile_name
|
|
|
|
|
|
|
|
if 'parent' not in profile:
|
|
|
|
raise KeyError(f'Profile attribute {attr_name} not found in {profile_name} and no parents')
|
|
|
|
parent = profile
|
|
|
|
parent_name = profile_name
|
|
|
|
seen = []
|
|
|
|
while True:
|
|
|
|
if attr_name in parent:
|
|
|
|
return parent[attr_name], parent_name
|
|
|
|
|
|
|
|
seen.append(parent_name)
|
|
|
|
|
|
|
|
if not parent.get('parent', None):
|
|
|
|
raise KeyError(f'Profile attribute {attr_name} not found in inheritance chain, '
|
|
|
|
f'we went down to {parent_name}.')
|
|
|
|
parent_name = parent['parent']
|
|
|
|
if parent_name in seen:
|
|
|
|
raise RecursionError(f"Profile recursion loop: profile {profile_name} couldn't be resolved"
|
|
|
|
f"because of a dependency loop:\n{' -> '.join([*seen, parent_name])}")
|
|
|
|
parent = profiles_sparse[parent_name]
|