Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add upgrading of linux kernel #76

Merged
merged 7 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus

from errors import MissingConfigurationError, RunnerError, SubprocessError
from errors import MissingConfigurationError, RunnerBinaryError, RunnerError, SubprocessError
from event_timer import EventTimer, TimerDisableError, TimerEnableError
from firewall import Firewall, FirewallEntry
from github_type import GitHubRunnerStatus
Expand All @@ -37,7 +37,7 @@
from utilities import bytes_with_unit_to_kib, execute_command, get_env_var, retry

if TYPE_CHECKING:
from ops.model import JsonObject # pragma: no cover
from ops.model import JsonObject # type: ignore
yhaliaw marked this conversation as resolved.
Show resolved Hide resolved

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -273,6 +273,12 @@ def _on_install(self, _event: InstallEvent) -> None:
"""
self.unit.status = MaintenanceStatus("Installing packages")

# Temporary solution: Upgrade the kernel due to a kernel bug in 5.15. Kernel upgrade
# not needed for container-based end-to-end tests.
if not LXD_PROFILE_YAML.exists():
self.unit.status = MaintenanceStatus("Upgrading kernel")
self._upgrade_kernel()

try:
# The `_start_services`, `_install_deps` includes retry.
self._install_deps()
Expand All @@ -294,12 +300,13 @@ def _on_install(self, _event: InstallEvent) -> None:
self._stored.runner_bin_url = runner_info.download_url
runner_manager.update_runner_bin(runner_info)
# Safe guard against transient unexpected error.
except Exception as err: # pylint: disable=broad-exception-caught
except RunnerBinaryError as err:
logger.exception("Failed to update runner binary")
# Failure to download runner binary is a transient error.
# The charm automatically update runner binary on a schedule.
self.unit.status = MaintenanceStatus(f"Failed to update runner binary: {err}")
return

self.unit.status = MaintenanceStatus("Starting runners")
try:
self._reconcile_runners(runner_manager)
Expand All @@ -310,6 +317,16 @@ def _on_install(self, _event: InstallEvent) -> None:
else:
self.unit.status = BlockedStatus("Missing token or org/repo path config")

def _upgrade_kernel(self) -> None:
"""Upgrade the Linux kernel."""
execute_command(["/usr/bin/apt-get", "update"])
execute_command(["/usr/bin/apt-get", "install", "-qy", "linux-generic-hwe-22.04"])

_, exit_code = execute_command(["ls", "/var/run/reboot-required"], check_exit=False)
if exit_code == 0:
logger.info("Rebooting system...")
execute_command(["reboot"])

@catch_charm_errors
def _on_upgrade_charm(self, _event: UpgradeCharmEvent) -> None:
"""Handle the update of charm.
Expand Down Expand Up @@ -574,6 +591,7 @@ def _install_deps(self) -> None:
env["NO_PROXY"] = self.proxies["no_proxy"]
env["no_proxy"] = self.proxies["no_proxy"]

execute_command(["/usr/bin/apt-get", "update"])
# install dependencies used by repo-policy-compliance and the firewall
execute_command(
["/usr/bin/apt-get", "install", "-qy", "gunicorn", "python3-pip", "nftables"]
Expand Down
6 changes: 3 additions & 3 deletions src/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_host_ip(self) -> str:
Returns:
The host IP address.
"""
address = execute_command(
address, _ = execute_command(
["/snap/bin/lxc", "network", "get", self._network, "ipv4.address"]
)
return str(ipaddress.IPv4Interface(address.strip()).ip)
Expand All @@ -75,7 +75,7 @@ def refresh_firewall(self, denylist: typing.List[FirewallEntry]):
current_acls = [
acl["name"]
for acl in yaml.safe_load(
execute_command(["lxc", "network", "acl", "list", "-f", "yaml"])
execute_command(["lxc", "network", "acl", "list", "-f", "yaml"])[0]
)
]
if self._ACL_RULESET_NAME not in current_acls:
Expand All @@ -99,7 +99,7 @@ def refresh_firewall(self, denylist: typing.List[FirewallEntry]):
]
)
acl_config = yaml.safe_load(
execute_command(["/snap/bin/lxc", "network", "acl", "show", self._ACL_RULESET_NAME])
execute_command(["/snap/bin/lxc", "network", "acl", "show", self._ACL_RULESET_NAME])[0]
)
host_ip = self.get_host_ip()
egress_rules = [
Expand Down
48 changes: 28 additions & 20 deletions src/runner_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,30 +190,40 @@ def update_runner_bin(self, binary: RunnerApplication) -> None:
"""
logger.info("Downloading runner binary from: %s", binary["download_url"])

# Delete old version of runner binary.
RunnerManager.runner_bin_path.unlink(missing_ok=True)
try:
# Delete old version of runner binary.
RunnerManager.runner_bin_path.unlink(missing_ok=True)
except OSError as err:
logger.exception("Unable to perform file operation on the runner binary path")
raise RunnerBinaryError("File operation failed on the runner binary path") from err

# Download the new file
response = self.session.get(binary["download_url"], stream=True)
try:
# Download the new file
response = self.session.get(binary["download_url"], stream=True)

logger.info(
"Download of runner binary from %s return status code: %i",
binary["download_url"],
response.status_code,
)
logger.info(
"Download of runner binary from %s return status code: %i",
binary["download_url"],
response.status_code,
)

if not binary["sha256_checksum"]:
logger.error("Checksum for runner binary is not found, unable to verify download.")
raise RunnerBinaryError("Checksum for runner binary is not found in GitHub response.")
if not binary["sha256_checksum"]:
logger.error("Checksum for runner binary is not found, unable to verify download.")
raise RunnerBinaryError(
"Checksum for runner binary is not found in GitHub response."
)

sha256 = hashlib.sha256()
sha256 = hashlib.sha256()

with RunnerManager.runner_bin_path.open(mode="wb") as file:
# Process with chunk_size of 128 KiB.
for chunk in response.iter_content(chunk_size=128 * 1024, decode_unicode=False):
file.write(chunk)
with RunnerManager.runner_bin_path.open(mode="wb") as file:
# Process with chunk_size of 128 KiB.
for chunk in response.iter_content(chunk_size=128 * 1024, decode_unicode=False):
file.write(chunk)

sha256.update(chunk)
sha256.update(chunk)
except requests.RequestException as err:
logger.exception("Failed to download of runner binary")
raise RunnerBinaryError("Failed to download runner binary") from err

logger.info("Finished download of runner binary.")

Expand All @@ -224,13 +234,11 @@ def update_runner_bin(self, binary: RunnerApplication) -> None:
binary["sha256_checksum"],
sha256,
)
RunnerManager.runner_bin_path.unlink(missing_ok=True)
raise RunnerBinaryError("Checksum mismatch for downloaded runner binary")

# Verify the file integrity.
if not tarfile.is_tarfile(file.name):
logger.error("Failed to decompress downloaded GitHub runner binary.")
RunnerManager.runner_bin_path.unlink(missing_ok=True)
raise RunnerBinaryError("Downloaded runner binary cannot be decompressed.")

logger.info("Validated newly downloaded runner binary and enabled it.")
Expand Down
13 changes: 6 additions & 7 deletions src/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def secure_run_subprocess(cmd: Sequence[str], **kwargs) -> subprocess.CompletedP
return result


def execute_command(cmd: Sequence[str], check_exit: bool = True, **kwargs) -> str:
def execute_command(cmd: Sequence[str], check_exit: bool = True, **kwargs) -> tuple[str, int]:
"""Execute a command on a subprocess.

The command is executed with `subprocess.run`, additional arguments can be passed to it as
Expand All @@ -136,7 +136,7 @@ def execute_command(cmd: Sequence[str], check_exit: bool = True, **kwargs) -> st
kwargs: Additional keyword arguments for the `subprocess.run` call.

Returns:
Output on stdout.
Output on stdout, and the exit code.
"""
result = secure_run_subprocess(cmd, **kwargs)

Expand All @@ -153,11 +153,10 @@ def execute_command(cmd: Sequence[str], check_exit: bool = True, **kwargs) -> st

raise SubprocessError(cmd, err.returncode, err.stdout, err.stderr) from err

return (
result.stdout
if isinstance(result.stdout, str)
else result.stdout.decode(kwargs.get("encoding", "utf-8"))
)
if isinstance(result.stdout, str):
return (result.stdout, result.returncode)

return (result.stdout.decode(kwargs.get("encoding", "utf-8")), result.returncode)


def get_env_var(env_var: str) -> Optional[str]:
Expand Down
Loading