Git bisect combinator can be used to loop the runbook
until the bisect operations suceeds.

The example provided is to bisect failure in kernel tree
This commit is contained in:
adityanagesh 2023-10-17 22:52:34 +05:30 коммит произвёл Aditya Nagesh
Родитель 0c8cd244df
Коммит f96d0ce4ea
8 изменённых файлов: 393 добавлений и 18 удалений

Просмотреть файл

@ -50,6 +50,7 @@ Runbook Reference
- `batch combinator <#batch-combinator>`__
- `items <#items-1>`__
- `bisect combinator <#bisect-combinator>`__
- `notifier <#notifier>`__
@ -571,6 +572,30 @@ For example,
- image: CentOS
vm_size: Standard_DS3_v2
bisect combinator
^^^^^^^^^^^^^^^^^
Specify a git repo url, the good commit and bad commit. The combinator
performs bisect operations on VM specified under 'connection'.
The runbook will be iterated until the bisect operations completes.
For example,
.. code:: yaml
combinator:
type: git_bisect
repo: $(repo_url)
bad_commit: $(bad_commit)
good_commit: $(good_commit)
connection:
address: $(bisect_vm_address)
private_key_file: $(admin_private_key_file)
Refer `Sample runbook <https://github.com/microsoft/lisa/blob/main/examples/runbook/git_bisect.yml>`__
notifier
~~~~~~~~

Просмотреть файл

@ -0,0 +1,191 @@
import pathlib
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Type
from dataclasses_json import dataclass_json
from lisa import messages, notifier, schema
from lisa.combinator import Combinator
from lisa.messages import KernelBuildMessage, TestResultMessage, TestStatus
from lisa.node import Node, quick_connect
from lisa.tools.git import Git, GitBisect
from lisa.util import LisaException, constants, field_metadata
STOP_PATTERNS = ["first bad commit", "This means the bug has been fixed between"]
# Combinator requires a node to clone the source code.
@dataclass_json()
@dataclass
class GitBisectCombinatorSchema(schema.Combinator):
connection: Optional[schema.RemoteNode] = field(
default=None, metadata=field_metadata(required=True)
)
repo: str = field(
default="",
metadata=field_metadata(
required=True,
),
)
good_commit: str = field(
default="",
metadata=field_metadata(
required=True,
),
)
bad_commit: str = field(
default="",
metadata=field_metadata(
required=True,
),
)
# GitBisect Combinator is a loop that runs "expanded" phase
# of runbook until the bisect is complete.
# There can be any number of expanded phases, but the
# GitBisectTestResult notifier should have on boolean/None output per
# phase.
class GitBisectCombinator(Combinator):
def __init__(
self,
runbook: GitBisectCombinatorSchema,
**kwargs: Any,
) -> None:
super().__init__(runbook)
self._iteration = 0
self._result_notifier = GitBisectResult(schema.Notifier())
notifier.register_notifier(self._result_notifier)
self._source_path: pathlib.PurePath
self._node: Optional[Node] = None
def _initialize(self, *args: Any, **kwargs: Any) -> None:
self._clone_source()
if self._source_path:
self._start_bisect()
else:
raise LisaException(
"Source path is not set. Please check the source clone."
)
@classmethod
def type_name(cls) -> str:
return constants.COMBINATOR_GITBISECT
@classmethod
def type_schema(cls) -> Type[schema.TypedSchema]:
return GitBisectCombinatorSchema
def _next(self) -> Optional[Dict[str, Any]]:
_next: Optional[Dict[str, Any]] = None
self._process_result()
if not self._check_bisect_complete():
_next = {}
_next["ref"] = self._get_current_commit_hash()
else:
self._log.info("Bisect Complete")
self._result_notifier.result = None
self._iteration += 1
return _next
def _process_result(self) -> None:
if self._iteration == 0:
return
if self._result_notifier.result is not None:
results = self._result_notifier.result
if results:
self._bisect_good()
else:
self._bisect_bad()
else:
raise LisaException(
"Bisect combinator does not get result for next iteration. Please check"
" GitBisectResult notifier."
)
def _get_remote_node(self) -> Node:
if not self._node or not self._node.is_connected:
self._node = quick_connect(self.runbook.connection, "source_node")
return self._node
def _clone_source(self) -> None:
node = self._get_remote_node()
git = node.tools[Git]
self._source_path = git.clone(
url=self.runbook.repo, cwd=node.working_path, timeout=1200
)
node.close()
def _start_bisect(self) -> None:
node = self._get_remote_node()
git_bisect = node.tools[GitBisect]
git_bisect.start(cwd=self._source_path)
git_bisect.good(cwd=self._source_path, ref=self.runbook.good_commit)
git_bisect.bad(cwd=self._source_path, ref=self.runbook.bad_commit)
node.close()
def _bisect_bad(self) -> None:
node = self._get_remote_node()
git_bisect = node.tools[GitBisect]
git_bisect.bad(cwd=self._source_path)
node.close()
def _bisect_good(self) -> None:
node = self._get_remote_node()
git_bisect = node.tools[GitBisect]
git_bisect.good(cwd=self._source_path)
node.close()
def _check_bisect_complete(self) -> bool:
node = self._get_remote_node()
git_bisect = node.tools[GitBisect]
result = git_bisect.check_bisect_complete(cwd=self._source_path)
node.close()
return result
def _get_current_commit_hash(self) -> str:
node = self._get_remote_node()
git = node.tools[Git]
result = git.get_current_commit_hash(cwd=self._source_path)
node.close()
return result
class GitBisectResult(notifier.Notifier):
@classmethod
def type_name(cls) -> str:
return "git_bisect_result"
@classmethod
def type_schema(cls) -> Type[schema.TypedSchema]:
return schema.Notifier
def _initialize(self, *args: Any, **kwargs: Any) -> None:
self.result: Optional[bool] = None
def _received_message(self, message: messages.MessageBase) -> None:
if isinstance(message, messages.TestResultMessage):
self._update_test_result(message)
elif isinstance(message, messages.KernelBuildMessage):
self._update_result(message.is_success)
else:
raise LisaException(f"Received unsubscribed message type: {type(message)}")
def _update_test_result(self, message: messages.TestResultMessage) -> None:
if message.is_completed:
if message.status == TestStatus.FAILED:
self._update_result(False)
elif message.status == TestStatus.PASSED:
self._update_result(True)
def _update_result(self, result: bool) -> None:
current_result = self.result
if current_result is not None:
self.result = current_result and result
else:
self.result = result
def _subscribed_message_type(self) -> List[Type[messages.MessageBase]]:
return [TestResultMessage, KernelBuildMessage]

Просмотреть файл

@ -263,6 +263,15 @@ class ProvisionBootTimeMessage(MessageBase):
information: Dict[str, str] = field(default_factory=dict)
@dataclass
class KernelBuildMessage(MessageBase):
type: str = "KernelBuild"
old_kernel_version: str = ""
new_kernel_version: str = ""
is_success: bool = False
error_message: str = ""
def _is_completed_status(status: TestStatus) -> bool:
return status in [
TestStatus.FAILED,

Просмотреть файл

@ -8,6 +8,7 @@ import platform
import lisa.combinators.batch_combinator # noqa: F401
import lisa.combinators.csv_combinator # noqa: F401
import lisa.combinators.git_bisect_combinator # noqa: F401
import lisa.combinators.grid_combinator # noqa: F401
import lisa.notifiers.console # noqa: F401
import lisa.notifiers.env_stats # noqa: F401

Просмотреть файл

@ -6,10 +6,11 @@ import re
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Type, cast
from assertpy import assert_that
from assertpy.assertpy import assert_that
from dataclasses_json import dataclass_json
from lisa import schema
from lisa import notifier, schema
from lisa.messages import KernelBuildMessage
from lisa.node import Node, quick_connect
from lisa.operating_system import Posix, Ubuntu
from lisa.secret import PATTERN_HEADTAIL, add_secret
@ -73,6 +74,7 @@ class KernelInstallerTransformerSchema(schema.Transformer):
installer: Optional[BaseInstallerSchema] = field(
default=None, metadata=field_metadata(required=True)
)
raise_exception: Optional[bool] = True
class BaseInstaller(subclasses.BaseClassWithRunbookMixin):
@ -101,6 +103,8 @@ class BaseInstaller(subclasses.BaseClassWithRunbookMixin):
class KernelInstallerTransformer(Transformer):
_information_output_name = "information"
_is_success_output_name = "is_success"
_information: Dict[str, Any] = dict()
@classmethod
@ -120,6 +124,10 @@ class KernelInstallerTransformer(Transformer):
assert runbook.connection, "connection must be defined."
assert runbook.installer, "installer must be defined."
message = KernelBuildMessage()
build_sucess: bool = False
boot_success: bool = False
node = quick_connect(runbook.connection, "installer_node")
uname = node.tools[Uname]
@ -133,24 +141,68 @@ class KernelInstallerTransformer(Transformer):
)
installer.validate()
installed_kernel_version = installer.install()
self._information = installer.information
self._log.info(f"installed kernel version: {installed_kernel_version}")
# for ubuntu cvm kernel, there is no menuentry added into grub file
if hasattr(installer.runbook, "source"):
if installer.runbook.source != "linux-image-azure-fde":
posix = cast(Posix, node.os)
posix.replace_boot_kernel(installed_kernel_version)
try:
message.old_kernel_version = uname.get_linux_information(
force_run=True
).kernel_version_raw
self._log.info("rebooting")
node.reboot()
kernel_version_after_install = uname.get_linux_information(force_run=True)
self._log.info(f"kernel version after install: {kernel_version_after_install}")
assert_that(
kernel_version_after_install, "Kernel installation Failed"
).is_not_equal_to(kernel_version_before_install)
return {self._information_output_name: self._information}
installed_kernel_version = installer.install()
build_sucess = True
self._information = installer.information
self._log.info(f"installed kernel version: {installed_kernel_version}")
# for ubuntu cvm kernel, there is no menuentry added into grub file
if hasattr(installer.runbook, "source"):
if installer.runbook.source != "linux-image-azure-fde":
posix = cast(Posix, node.os)
posix.replace_boot_kernel(installed_kernel_version)
else:
efi_files = node.execute(
"ls -t /usr/lib/linux/efi/kernel.efi-*-azure-cvm",
sudo=True,
shell=True,
expected_exit_code=0,
expected_exit_code_failure_message=(
"fail to find kernel.efi file for kernel type "
" linux-image-azure-fde"
),
)
efi_file = efi_files.stdout.splitlines()[0]
node.execute(
(
"cp /boot/efi/EFI/ubuntu/grubx64.efi "
"/boot/efi/EFI/ubuntu/grubx64.efi.bak"
),
sudo=True,
)
node.execute(
f"cp {efi_file} /boot/efi/EFI/ubuntu/grubx64.efi",
sudo=True,
shell=True,
)
self._log.info("rebooting")
node.reboot()
boot_success = True
new_kernel_version = uname.get_linux_information(force_run=True)
message.new_kernel_version = new_kernel_version.kernel_version_raw
self._log.info(f"kernel version after install: " f"{new_kernel_version}")
assert_that(
new_kernel_version.kernel_version_raw, "Kernel installation Failed"
).is_not_equal_to(kernel_version_before_install.kernel_version_raw)
except Exception as e:
message.error_message = str(e)
if runbook.raise_exception:
raise e
self._log.info(f"Kernel build failed: {e}")
finally:
message.is_success = build_sucess and boot_success
notifier.notify(message)
return {
self._information_output_name: self._information,
self._is_success_output_name: build_sucess and boot_success,
}
class RepoInstaller(BaseInstaller):

Просмотреть файл

@ -84,6 +84,7 @@ TRANSFORMER_PHASE_CLEANUP = "cleanup"
COMBINATOR = "combinator"
COMBINATOR_GRID = "grid"
COMBINATOR_BATCH = "batch"
COMBINATOR_GITBISECT = "git_bisect"
ENVIRONMENT = "environment"
ENVIRONMENTS = "environments"

Просмотреть файл

@ -0,0 +1,95 @@
name: git_bisect
extension:
- "../../testsuites"
include:
- path: ../tiers/tier.yml
- path: ../azure.yml
variable:
- name: subscription_id
value: ""
- name: tier
value: 0
- name: test_case_name
value: "smoke_test"
- name: marketplace_image
value: "canonical ubuntuserver 18.04-lts latest"
- name: repo_url
value: "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git"
- name: good_commit
value: ""
- name: bad_commit
value: ""
# Do not assign values to below variables. These are used by the combinator.
- name: build_vm_address
value: ""
- name: bisect_vm_address
value: ""
- name: build_vm_resource_group_name
value: ""
- name: bisect_vm_resource_group_name
value: ""
- name: vhd
value: ""
- name: kernel_installer_is_success
value: False
- name: ref
value: ""
transformer:
- type: azure_deploy
name: bisect_vm
requirement:
azure:
marketplace: $(marketplace_image)
location: $(location)
core_count: 2
enabled: true
- type: azure_deploy
phase: expanded
name: build_vm
requirement:
azure:
marketplace: $(marketplace_image)
location: $(location)
core_count: 16
enabled: true
- type: kernel_installer
phase: expanded
connection:
address: $(build_vm_address)
private_key_file: $(admin_private_key_file)
installer:
type: source
location:
type: repo
path: /mnt/code
ref: $(ref)
repo: $(repo_url)
raise_exception: False
rename:
kernel_installer_is_success: enable_tests
# Do not create vhd when build fails
- type: azure_vhd
enabled: $(enable_tests)
phase: expanded
resource_group_name: $(build_vm_resource_group_name)
rename:
azure_vhd_url: vhd
- type: azure_delete
resource_group_name: $(build_vm_resource_group_name)
phase: expanded_cleanup
- type: azure_delete
resource_group_name: $(bisect_vm_resource_group_name)
phase: cleanup
combinator:
type: git_bisect
repo: $(repo_url)
bad_commit: $(bad_commit)
good_commit: $(good_commit)
connection:
address: $(bisect_vm_address)
private_key_file: $(admin_private_key_file)

Просмотреть файл

@ -15,3 +15,4 @@ testcase:
retry: $(retry)
use_new_environment: $(use_new_environment)
ignore_failure: $(ignore_failure)
enabled: $(enable_tests)