"""
"InheritableItem" the entry point for items that have logical parents and children.
Changelog:
* V3.4.0 (unreleased):
* Initial creation of the class
"""
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Enno Gotthold <enno.gotthold@suse.com>
from abc import ABC
from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Union
from cobbler import enums
from cobbler.cexceptions import CX
from cobbler.items.abstract.base_item import BaseItem
if TYPE_CHECKING:
from cobbler.api import CobblerAPI
from cobbler.items.distro import Distro
from cobbler.items.menu import Menu
from cobbler.items.profile import Profile
from cobbler.items.system import System
from cobbler.settings import Settings
LazyProperty = property
else:
from cobbler.decorator import LazyProperty
[docs]
class HierarchyItem(NamedTuple):
"""
NamedTuple to display the dependency that a single item has.
The `dependant_item_type` object is stored in the `dependant_type_attribute` attribute of the Item.
For example, an item with HierarchyItem("profile", "repos") contains `Profile` objects in the `repos` attribute.
"""
dependant_item_type: str
dependant_type_attribute: str
[docs]
class LogicalHierarchy(NamedTuple):
"""
NamedTuple to display the order of hierarchy in the dependency tree.
"""
up: List[HierarchyItem]
down: List[HierarchyItem]
[docs]
class InheritableItem(BaseItem, ABC):
"""
Abstract class that acts as a starting point in the inheritance for items that have a parent and children.
"""
# Constants
TYPE_NAME = "inheritable_abstract"
COLLECTION_TYPE = "inheritable_abstract"
# Item types dependencies.
# Used to determine descendants and cache invalidation.
# Format: {"Item Type": [("Dependent Item Type", "Dependent Type attribute"), ..], [..]}
TYPE_DEPENDENCIES: Dict[str, List[HierarchyItem]] = {
"repo": [
HierarchyItem("profile", "repos"),
],
"distro": [
HierarchyItem("profile", "distro"),
HierarchyItem("distro_group", "members"),
],
"distro_group": [
HierarchyItem("distro_group", "parent"),
],
"menu": [
HierarchyItem("menu", "parent"),
HierarchyItem("image", "menu"),
HierarchyItem("profile", "menu"),
],
"network_interface": [],
"profile": [
HierarchyItem("profile", "parent"),
HierarchyItem("system", "profile"),
HierarchyItem("profile_group", "members"),
],
"profile_group": [
HierarchyItem("profile_group", "parent"),
],
"image": [
HierarchyItem("system", "image"),
],
"system": [
HierarchyItem("network_interface", "system_uid"),
HierarchyItem("system_group", "members"),
],
"system_group": [
HierarchyItem("system_group", "parent"),
],
}
# Defines a logical hierarchy of Item Types.
# Format: {"Item Type": [("Previous level Type", "Attribute to go to the previous level",), ..],
# [("Next level Item Type", "Attribute to move from the next level"), ..]}
LOGICAL_INHERITANCE: Dict[str, LogicalHierarchy] = {
"distro": LogicalHierarchy(
[],
[
HierarchyItem("profile", "distro"),
],
),
"distro_group": LogicalHierarchy(
[
HierarchyItem("distro_group", "parent"),
],
[
HierarchyItem("distro_group", "parent"),
],
),
"profile": LogicalHierarchy(
[
HierarchyItem("distro", "distro"),
],
[
HierarchyItem("system", "profile"),
],
),
"profile_group": LogicalHierarchy(
[
HierarchyItem("profile_group", "parent"),
],
[
HierarchyItem("profile_group", "parent"),
],
),
"image": LogicalHierarchy(
[],
[
HierarchyItem("system", "image"),
],
),
"system": LogicalHierarchy(
[
HierarchyItem("image", "image"),
HierarchyItem("profile", "profile"),
],
[],
),
"system_group": LogicalHierarchy(
[
HierarchyItem("system_group", "parent"),
],
[
HierarchyItem("system_group", "parent"),
],
),
}
def __init__(
self, api: "CobblerAPI", *args: Any, is_subobject: bool = False, **kwargs: Any
):
"""
Constructor. Requires a back reference to the CobblerAPI object.
NOTE: is_subobject is used for objects that allow inheritance in their trees. This inheritance refers to
conceptual inheritance, not Python inheritance. Objects created with is_subobject need to call their
setter for parent immediately after creation and pass in a value of an object of the same type. Currently this
is only supported for profiles. Sub objects blend their data with their parent objects and only require a valid
parent name and a name for themselves, so other required options can be gathered from items further up the
Cobbler tree.
distro
profile
profile <-- created with is_subobject=True
system <-- created as normal
image
system
menu
menu
For consistency, there is some code supporting this in all object types, though it is only usable
(and only should be used) for profiles at this time. Objects that are children of
objects of the same type (i.e. subprofiles) need to pass this in as True. Otherwise, just
use False for is_subobject and the parent object will (therefore) have a different type.
The keyword arguments are used to seed the object. This is the preferred way over ``from_dict`` starting with
Cobbler version 3.4.0.
:param api: The Cobbler API object which is used for resolving information.
:param is_subobject: See above extensive description.
"""
super().__init__(api, *args, **kwargs)
self._depth = 0
self._parent = ""
self._is_subobject = is_subobject
self._children: List[str] = []
self._inmemory = True
if len(kwargs) > 0:
kwargs.update({"is_subobject": is_subobject})
self.from_dict(kwargs)
if not self._has_initialized:
self._has_initialized = True
@LazyProperty
def depth(self) -> int:
"""
This represents the logical depth of an object in the category of the same items. Important for the order of
loading items from the disk and other related features where the alphabetical order is incorrect for sorting.
:getter: The logical depth of the object.
:setter: The new int for the logical object-depth.
"""
return self._depth
@depth.setter
def depth(self, depth: int) -> None:
"""
Setter for depth.
:param depth: The new value for depth.
"""
if not isinstance(depth, int): # type: ignore
raise TypeError("depth needs to be of type int")
self._depth = depth
@LazyProperty
def parent(self) -> Optional[Union["System", "Profile", "Distro", "Menu"]]:
"""
This property contains the name of the parent of an object. In case there is not parent this return
None.
:getter: Returns the parent object or None if it can't be resolved via the Cobbler API.
:setter: The uid of the new logical parent.
"""
if self._parent == "":
return None
return self.api.get_items(self.COLLECTION_TYPE).listing.get(self._parent) # type: ignore
@parent.setter
def parent(self, parent: Union["InheritableItem", str]) -> None:
"""
Set the parent object for this object.
:param parent: The new parent object. This needs to be a descendant in the logical inheritance chain.
"""
if not isinstance(parent, str) and not isinstance(parent, InheritableItem): # type: ignore
raise TypeError('Property "parent" must be of type InheritableItem or str!')
found = None
if isinstance(parent, InheritableItem):
found = parent
parent = parent.uid
old_parent = self._parent
if self.TYPE_NAME == "profile": # type: ignore[reportUnnecessaryComparison]
old_arch: Optional[enums.Archs] = getattr(self, "arch")
new_arch: Optional[enums.Archs] = None
items = self.api.get_items(self.COLLECTION_TYPE)
if not parent:
self._parent = ""
items.update_index_value(self, "parent", old_parent, "")
if self.TYPE_NAME == "profile": # type: ignore[reportUnnecessaryComparison]
new_arch = getattr(self, "arch")
if new_arch != old_arch: # type: ignore[reportPossiblyUnboundVariable]
items.update_index_value(self, "arch", old_arch, new_arch) # type: ignore[reportArgumentType]
for child in self.tree_walk():
items.update_index_value(child, "arch", old_arch, new_arch) # type: ignore[reportArgumentType]
return
if parent == self.uid:
# check must be done in two places as setting parent could be called before/after setting name...
raise CX("self parentage is forbidden")
if found is None:
found = items.listing.get(parent) # type: ignore
if found is None:
raise CX(f'parent item "{parent}" not found, inheritance not possible')
self._parent = parent
self.depth = found.depth + 1 # type: ignore
items.update_index_value(self, "parent", old_parent, parent)
if self.TYPE_NAME == "profile": # type: ignore[reportUnnecessaryComparison]
new_arch = getattr(self, "arch")
if new_arch != old_arch: # type: ignore[reportPossiblyUnboundVariable]
items.update_index_value(self, "arch", old_arch, new_arch) # type: ignore[reportArgumentType]
for child in self.tree_walk():
items.update_index_value(child, "arch", old_arch, new_arch) # type: ignore[reportArgumentType]
@LazyProperty
def get_parent(self) -> str:
"""
This method returns the name of the parent for the object. In case there is not parent this return
empty string.
"""
return self._parent
[docs]
def get_conceptual_parent(self) -> Optional["InheritableItem"]:
"""
The parent may just be a superclass for something like a sub-profile. Get the first parent of a different type.
:return: The first item which is conceptually not from the same type.
"""
if self is None: # type: ignore
return None
curr_obj = self
next_obj = curr_obj.parent
while next_obj is not None:
curr_obj = next_obj
next_obj = next_obj.parent
if curr_obj.TYPE_NAME in curr_obj.LOGICAL_INHERITANCE:
for prev_level in curr_obj.LOGICAL_INHERITANCE[curr_obj.TYPE_NAME].up:
prev_level_type = prev_level.dependant_item_type
prev_level_uid = getattr(
curr_obj, "_" + prev_level.dependant_type_attribute
)
if prev_level_uid is not None and prev_level_uid != "":
prev_level_item = self.api.find_items(
prev_level_type, {"uid": prev_level_uid}, return_list=False
)
if prev_level_item is not None and not isinstance(
prev_level_item, list
):
return prev_level_item
return None
@property
def logical_parent(self) -> Any:
"""
This property contains the name of the logical parent of an object. In case there is not parent this return
None.
.. note:: This is a read only property.
:getter: Returns the parent object or None if it can't be resolved via the Cobbler API.
"""
parent = self.parent
if parent is None:
return self.get_conceptual_parent()
return parent
@property
def children(self) -> List["InheritableItem"]:
"""
The list of logical children of any depth.
.. note:: This is a read only property.
:getter: An empty list in case of items which don't have logical children.
"""
if self.COLLECTION_TYPE not in ["profile", "menu"]:
return []
results: Optional[List["InheritableItem"]] = self.api.find_items( # type: ignore
self.COLLECTION_TYPE, {"parent": self._uid}, return_list=True
)
if results is None:
return []
return results
[docs]
def tree_walk(
self, attribute_name: Optional[str] = None
) -> List["InheritableItem"]:
"""
Get all children related by parent/child relationship.
:return: The list of children objects.
"""
results: List["InheritableItem"] = []
for child in self.children:
if (
attribute_name is None
or getattr(child, attribute_name) == enums.VALUE_INHERITED
):
results.append(child)
results.extend(child.tree_walk(attribute_name))
return results
@property
def descendants(self) -> List["InheritableItem"]:
"""
Get objects that depend on this object, i.e. those that would be affected by a cascading delete, etc.
.. note:: This is a read only property.
:getter: This is a list of all descendants. May be empty if none exist.
"""
childs = self.tree_walk()
results = set(childs)
childs.append(self) # type: ignore
for child in childs:
for item_type in self.TYPE_DEPENDENCIES[child.COLLECTION_TYPE]:
dep_type_items = self.api.find_items(
item_type.dependant_item_type,
{item_type.dependant_type_attribute: child.uid},
return_list=True,
)
if dep_type_items is None or not isinstance(dep_type_items, list):
raise ValueError("Expected list to be returned by find_items")
results.update(dep_type_items)
for dep_item in dep_type_items:
if isinstance(dep_item, InheritableItem): # type: ignore
results.update(dep_item.descendants)
return list(results)
@LazyProperty
def is_subobject(self) -> bool:
"""
Weather the object is a subobject of another object or not.
:getter: True in case the object is a subobject, False otherwise.
:setter: Sets the value. If this is not a bool, this will raise a ``TypeError``.
"""
return self._is_subobject
@is_subobject.setter
def is_subobject(self, value: bool) -> None:
"""
Setter for the property ``is_subobject``.
:param value: The boolean value whether this is a subobject or not.
:raises TypeError: In case the value was not of type bool.
"""
if not isinstance(value, bool): # type: ignore
raise TypeError(
"Field is_subobject of object item needs to be of type bool!"
)
self._is_subobject = value
[docs]
def grab_tree(self) -> List[Union["InheritableItem", "Settings"]]:
"""
Climb the tree and get every node.
:return: The list of items with all parents from that object upwards the tree. Contains at least the item
itself and the settings of Cobbler.
"""
results: List[Union["InheritableItem", "Settings"]] = [self]
parent = self.logical_parent
while parent is not None:
results.append(parent)
parent = parent.logical_parent
# FIXME: Now get the object and check its existence
results.append(self.api.settings())
self.logger.debug(
"grab_tree found %s children (including settings) of this object",
len(results),
)
return results