Skip to content

Commit

Permalink
Add upgrading of linux kernel (#76)
Browse files Browse the repository at this point in the history
* Add upgrading of linux kernel

* Refactor download of runner binary to not catch base Exception.

* Modify execute_command to return exit code.
  • Loading branch information
yhaliaw authored Jul 4, 2023
1 parent ff2e6d8 commit dbe393d
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 33 deletions.
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

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

0 comments on commit dbe393d

Please sign in to comment.