Skip to content

Commit

Permalink
✨ Make LocalNode and RemoteNode extendable classes (yml-wise).
Browse files Browse the repository at this point in the history
We do so by making use of BaseClassWithRunbookMixin, on the actual
instance classes, and ExtendableSchemaMixin, on the respective schema
classes. This makes it possible to have custom, type-defined sections
on general (SUT) nodes coming from the runbook, together with Platform
and Notifier.

This will come in handy for LISA users who have node logic/fields that
go beyond what is originally defined to work for (e.g.) Azure.

Example of power/usage:

By declaring this, on code:

@dataclass_json()
@DataClass
class MyNodeSchema(schema.RemoteNode):
    type: str = field(
        default=MYNAME,
        metadata=schema.metadata(
            required=True,
            validate=validate.OneOf([MYNAME]),
        ),
    )

    my_example_extra_field: Optional[str] = field(default=None)

class MyNode(node.RemoteNode):
    def __init__(
        self,
        index: int,
        runbook: MyNodeSchema,
        logger_name: str,
        base_log_path: Optional[Path] = None,
        name: str = "",
    ) -> None:
        super().__init__(index, runbook,
                         logger_name=logger_name,
                         base_log_path=base_log_path,
                         name=name)
        self.my_example_extra_field = runbook.my_example_extra_field
        assert self.my_example_extra_field, \
            f"my_example_extra_field field of {MYNAME}-typed " \
            "nodes cannot be empty "

    @classmethod
    def type_name(cls) -> str:
        return MYNAME

    @classmethod
    def type_schema(cls) -> Type[schema.TypedSchema]:
        return MyNodeSchema

one is able to do this, yml-wise:

environment:
  warn_as_error: true
  environments:
    - nodes:
      - type: MYNAME
        public_address: ...
        public_port: ...
        username: ...
        password: ...
->      my_example_extra_field: ...

Of course, custom logic for only that type of node will be at your
fingertips, just by extending/overriding your node class. Of course
this is advanced usage and not meant for the average user.

UTs were added to help enforce regression testing.

ammend to nodes
  • Loading branch information
glima committed Apr 23, 2021
1 parent ea1a4ae commit cb4f983
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 260 deletions.
3 changes: 2 additions & 1 deletion examples/testsuites/withscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from lisa import Node, TestCaseMetadata, TestSuite, TestSuiteMetadata
from lisa.executable import CustomScript, CustomScriptBuilder
from lisa.node import is_remote
from lisa.operating_system import Windows
from lisa.testsuite import simple_requirement
from lisa.util.perf_timer import create_timer
Expand Down Expand Up @@ -44,7 +45,7 @@ def script(self, node: Node) -> None:
timer2 = create_timer()
result2 = script.run(force_run=True)
assert_that(result1.stdout).is_equal_to(result2.stdout)
if node.is_remote:
if is_remote(node):
# the timer will be significant different on a remote node.
assert_that(
timer1.elapsed(), "the second time should be faster, without uploading"
Expand Down
18 changes: 12 additions & 6 deletions lisa/executable.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from hashlib import sha256
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union, cast

from lisa.schema import RemoteNode
from lisa.util import InitializableMixin, LisaException, constants
from lisa.util.logger import get_logger
from lisa.util.perf_timer import create_timer
Expand All @@ -20,6 +21,11 @@
T = TypeVar("T")


# circular dep if we use the helper in node.py, unfortunately
def _is_node_remote(node: Node) -> bool:
return isinstance(node.type_schema, RemoteNode)


class Tool(ABC, InitializableMixin):
"""
The base class, which wraps an executable, package, or scripts on a node.
Expand Down Expand Up @@ -246,10 +252,10 @@ def run(

def get_tool_path(self) -> pathlib.PurePath:
"""
compose a path, if the tool need to be installed
compose a path, if the tool needs to be installed
"""
assert self.node.remote_working_path, "remote working path is not initialized"
return self.node.remote_working_path.joinpath(constants.PATH_TOOL, self.name)
assert self.node.working_path, "working path is not initialized"
return self.node.working_path.joinpath(constants.PATH_TOOL, self.name)

def __call__(
self,
Expand Down Expand Up @@ -359,7 +365,7 @@ def dependencies(self) -> List[Type[Tool]]:
return self._dependencies

def install(self) -> bool:
if self.node.is_remote:
if _is_node_remote(self.node):
# copy to remote
node_script_path = self.get_tool_path()
for file in self._files:
Expand Down Expand Up @@ -495,15 +501,15 @@ def __getitem__(self, tool_type: Union[Type[T], CustomScriptBuilder, str]) -> T:
is_success = tool.install()
if not is_success:
raise LisaException(
f"install '{tool.name}' failed. After installed, "
f"installing '{tool.name}' has failed. After installed, "
f"it cannot be detected."
)
tool_log.debug(f"installed in {timer}")
else:
raise LisaException(
f"cannot find [{tool.name}] on [{self._node.name}], "
f"{self._node.os.__class__.__name__}, "
f"Remote({self._node.is_remote}) "
f"Remote({_is_node_remote(self._node)}) "
f"and installation of [{tool.name}] isn't enabled in lisa."
)
else:
Expand Down
Loading

0 comments on commit cb4f983

Please sign in to comment.