Skip to content
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
139 changes: 139 additions & 0 deletions scripts/sonic-bootchart
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env python3

import click
import sys
import configparser
import functools
import os
import glob
from tabulate import tabulate
import utilities_common.cli as clicommon

SYSTEMD_BOOTCHART = "/lib/systemd/systemd-bootchart"
BOOTCHART_CONF = "/etc/systemd/bootchart.conf"
BOOTCHART_DEFAULT_OUTPUT_DIR = "/run/log/"
BOOTCHART_DEFAULT_OUTPUT_GLOB = os.path.join(BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-*.svg")

class BootChartConfigParser(configparser.ConfigParser):
""" Custom bootchart config parser. Changes the way ConfigParser passes options """

def optionxform(self, option):
""" Pass options as is, without modifications """
return option


def exit_cli(*args, **kwargs):
""" Print a message and exit with rc 1. """
click.secho(*args, **kwargs)
sys.exit(1)


def root_privileges_required(func):
""" Decorates a function, so that the function is invoked
only if the user is root. """
@functools.wraps(func)
def wrapped_function(*args, **kwargs):
""" Wrapper around func. """
if os.geteuid() != 0:
exit_cli("Root privileges required for this operation", fg="red")
return func(*args, **kwargs)

wrapped_function.__doc__ += "\n\n NOTE: This command requires elevated (root) privileges to run."
return wrapped_function


def check_bootchart_installed():
""" Fails imidiatelly if bootchart is not installed """
if not os.path.exists(SYSTEMD_BOOTCHART):
exit_cli("systemd-bootchart is not installed", fg="red")


def get_enabled_status():
""" Get systemd-bootchart status """
return clicommon.run_command("systemctl is-enabled systemd-bootchart", return_cmd=True)

def get_active_status():
""" Get systemd-bootchart status """
return clicommon.run_command("systemctl is-active systemd-bootchart", return_cmd=True)

def get_output_files():
bootchart_output_files = []
for bootchart_output_file in glob.glob(BOOTCHART_DEFAULT_OUTPUT_GLOB):
bootchart_output_files.append(bootchart_output_file)
return "\n".join(bootchart_output_files)


@click.group()
def cli():
""" Main CLI group """
check_bootchart_installed()


@cli.command()
@root_privileges_required
def enable():
""" Enable bootchart """
clicommon.run_command("systemctl enable systemd-bootchart", display_cmd=True)


@cli.command()
@root_privileges_required
def disable():
""" Disable bootchart """
clicommon.run_command("systemctl disable systemd-bootchart", display_cmd=True)


@cli.command()
@click.option('--time', type=click.IntRange(min=1), required=True)
@click.option('--frequency', type=click.IntRange(min=1), required=True)
@root_privileges_required
def config(time, frequency):
""" Configure bootchart """
samples = time * frequency

config = {
'Samples': str(samples),
'Frequency': str(frequency),
}
bootchart_config = BootChartConfigParser()
bootchart_config.read(BOOTCHART_CONF)
bootchart_config['Bootchart'].update(config)
with open(BOOTCHART_CONF, 'w') as config_file:
bootchart_config.write(config_file, space_around_delimiters=False)


@cli.command()
def show():
""" Display bootchart configuration """
bootchart_config = BootChartConfigParser()
bootchart_config.read(BOOTCHART_CONF)

try:
samples = int(bootchart_config["Bootchart"]["Samples"])
frequency = int(bootchart_config["Bootchart"]["Frequency"])
except KeyError as key:
raise click.ClickException(f"Failed to parse bootchart config: {key} not found")
except ValueError as err:
raise click.ClickException(f"Failed to parse bootchart config: {err}")

try:
time = samples // frequency
except ZeroDivisionError:
raise click.ClickException(f"Invalid frequency value: {frequency}")

field_values = {
"Status": get_enabled_status(),
"Operational Status": get_active_status(),
"Frequency": frequency,
"Time (sec)": time,
"Output": get_output_files(),
}

click.echo(tabulate([field_values.values()], field_values.keys()))


def main():
cli()

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
'scripts/watermarkstat',
'scripts/watermarkcfg',
'scripts/sonic-kdump-config',
'scripts/sonic-bootchart',
'scripts/centralize_database',
'scripts/null_route_helper',
'scripts/coredump_gen_handler.py',
Expand Down
124 changes: 124 additions & 0 deletions tests/sonic_bootchart_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import os
import subprocess
import pytest
from click.testing import CliRunner
from unittest.mock import patch, Mock
import utilities_common
import imp

sonic_bootchart = imp.load_source('sonic-bootchart', 'scripts/sonic-bootchart')

BOOTCHART_OUTPUT_FILES = [
os.path.join(sonic_bootchart.BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-20220504-1040.svg"),
os.path.join(sonic_bootchart.BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-20220504-1045.svg"),
]

@pytest.fixture(autouse=True)
def setup(fs):
# create required file for bootchart installation check
fs.create_file(sonic_bootchart.SYSTEMD_BOOTCHART)
fs.create_file(sonic_bootchart.BOOTCHART_CONF)
for bootchart_output_file in BOOTCHART_OUTPUT_FILES:
fs.create_file(bootchart_output_file)

with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
config_file.write("""
[Bootchart]
Samples=500
Frequency=25
""")

# pass the root user check
with patch("os.geteuid") as mock:
mock.return_value = 0
yield


@patch("utilities_common.cli.run_command")
class TestSonicBootchart:
def test_enable(self, mock_run_command):
runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['enable'], [])
assert not result.exit_code
mock_run_command.assert_called_with("systemctl enable systemd-bootchart", display_cmd=True)

def test_disable(self, mock_run_command):
runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['disable'], [])
assert not result.exit_code
mock_run_command.assert_called_with("systemctl disable systemd-bootchart", display_cmd=True)

def test_config_show(self, mock_run_command):
def run_command_side_effect(command, **kwargs):
if "is-enabled" in command:
return "enabled"
elif "is-active" in command:
return "active"
else:
raise Exception("unknown command")

mock_run_command.side_effect = run_command_side_effect

runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
assert not result.exit_code
assert result.output == \
"Status Operational Status Frequency Time (sec) Output\n" \
"-------- -------------------- ----------- ------------ ------------------------------------\n" \
"enabled active 25 20 /run/log/bootchart-20220504-1040.svg\n" \
" /run/log/bootchart-20220504-1045.svg\n"

result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "2", "--frequency", "50"])
assert not result.exit_code

result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
assert not result.exit_code
assert result.output == \
"Status Operational Status Frequency Time (sec) Output\n" \
"-------- -------------------- ----------- ------------ ------------------------------------\n" \
"enabled active 50 2 /run/log/bootchart-20220504-1040.svg\n" \
" /run/log/bootchart-20220504-1045.svg\n"

# Input validation tests

result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "0", "--frequency", "50"])
assert result.exit_code

result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "2", "--frequency", "-5"])
assert result.exit_code

def test_invalid_config_show(self, mock_run_command):
with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
config_file.write("""
[Bootchart]
Samples=100
""")

runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
assert result.exit_code
assert result.output == "Error: Failed to parse bootchart config: 'Frequency' not found\n"

with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
config_file.write("""
[Bootchart]
Samples=abc
Frequency=def
""")

runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
assert result.exit_code
assert result.output == "Error: Failed to parse bootchart config: invalid literal for int() with base 10: 'abc'\n"

with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
config_file.write("""
[Bootchart]
Samples=100
Frequency=0
""")

runner = CliRunner()
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
assert result.exit_code
assert result.output == "Error: Invalid frequency value: 0\n"