# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import copy
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Optional,
Type,
TypeVar,
Union,
cast,
)
from lisa import schema
from lisa.util import (
InitializableMixin,
LisaException,
NotMeetRequirementException,
constants,
)
from lisa.util.logger import get_logger
if TYPE_CHECKING:
from lisa.node import Node
from lisa.platform_ import Platform
[docs]
class Feature(InitializableMixin):
def __init__(
self, settings: schema.FeatureSettings, node: "Node", platform: "Platform"
) -> None:
super().__init__()
self._settings = settings
self._node: Node = node
self._platform: Platform = platform
self._log = get_logger("feature", self.name(), self._node.log)
[docs]
@classmethod
def settings_type(cls) -> Type[schema.FeatureSettings]:
return schema.FeatureSettings
[docs]
@classmethod
def name(cls) -> str:
return cls.__name__
[docs]
@classmethod
def can_disable(cls) -> bool:
raise NotImplementedError()
[docs]
def enabled(self) -> bool:
raise NotImplementedError()
[docs]
@classmethod
def get_feature_settings(
cls, feature: Union[Type["Feature"], schema.FeatureSettings, str]
) -> schema.FeatureSettings:
if isinstance(feature, Feature):
return feature._settings
if isinstance(feature, type):
return schema.FeatureSettings.create(feature.name())
elif isinstance(feature, str):
return schema.FeatureSettings.create(feature)
elif isinstance(feature, schema.FeatureSettings):
return feature
else:
raise LisaException(f"unsupported feature setting type: {type(feature)}")
[docs]
@classmethod
def on_before_deployment(cls, *args: Any, **kwargs: Any) -> None:
"""
If a feature need to change something before deployment, it needs to
implement this method. When this method is called, determined by the
platform.
"""
...
[docs]
@classmethod
def create_setting(
cls, *args: Any, **kwargs: Any
) -> Optional[schema.FeatureSettings]:
"""
It's called in platform to check if a node support the feature or not.
If it's supported, create a setting.
"""
return None
[docs]
def _initialize(self, *args: Any, **kwargs: Any) -> None:
"""
override for initializing
"""
...
[docs]
def get_settings(self) -> schema.FeatureSettings:
"""
Returns a read-only copy of the feature settings.
Modifications to the returned settings won't be effective.
"""
return copy.deepcopy(self._settings)
T_FEATURE = TypeVar("T_FEATURE", bound=Feature)
class Features:
def __init__(self, node: "Node", platform: "Platform") -> None:
self._node = node
self._platform = platform
self._feature_cache: Dict[str, Feature] = {}
self._feature_types: Dict[str, Type[Feature]] = {}
self._feature_settings: Dict[str, schema.FeatureSettings] = {}
for feature_type in platform.supported_features():
self._feature_types[feature_type.name()] = feature_type
if node.capability.features:
for feature_settings in node.capability.features:
self._feature_settings[feature_settings.type] = feature_settings
if node.capability.disk:
self._feature_settings[constants.FEATURE_DISK] = node.capability.disk
def is_supported(self, feature_type: Type[T_FEATURE]) -> bool:
return feature_type.name() in self._feature_types
def __getitem__(self, feature_type: Type[T_FEATURE]) -> T_FEATURE:
feature_name = feature_type.name()
feature: Optional[Feature] = self._feature_cache.get(feature_name, None)
if feature is None:
registered_feature_type = self._feature_types.get(feature_name)
if not registered_feature_type:
raise LisaException(
f"feature [{feature_name}] isn't supported on "
f"platform [{self._platform.type_name()}]"
)
settings = self._feature_settings.get(feature_name, None)
if not settings:
# feature is not specified, but should exists
settings = schema.FeatureSettings.create(feature_name)
settings_type = registered_feature_type.settings_type()
settings = schema.load_by_type(settings_type, settings)
feature = registered_feature_type(
settings=settings, node=self._node, platform=self._platform
)
feature.initialize()
self._feature_cache[feature_type.name()] = feature
assert feature
return cast(T_FEATURE, feature)
def get_feature_settings_type_by_name(
feature_name: str, features: Iterable[Type[Feature]]
) -> Type[schema.FeatureSettings]:
for feature in features:
if feature.name() == feature_name:
return feature.settings_type()
raise NotMeetRequirementException(
f"cannot find feature settings "
f"for '{feature_name}' in {[x.name() for x in features]}"
)
def get_feature_settings_by_name(
feature_name: str,
feature_settings: Iterable[schema.FeatureSettings],
ignore_error: bool = False,
) -> Optional[schema.FeatureSettings]:
assert feature_settings is not None, "not found features to query"
for single_setting in feature_settings:
if single_setting.type == feature_name:
return single_setting
if not ignore_error:
raise NotMeetRequirementException(
f"cannot find feature with type '{feature_name}' in {feature_settings}"
)
return None