Skip to content

Commit

Permalink
[sonic-bootchart] add sonic-bootchart (#2195)
Browse files Browse the repository at this point in the history
- What I did
Implemented sonic-net/SONiC#1001

- How I did it
Added a new sonic-bootchart script and added UT for it

- How to verify it
Run on the switch. Depends on sonic-net/sonic-buildimage#11047

Signed-off-by: Stepan Blyschak <stepanb@nvidia.com>
  • Loading branch information
stepanblyschak authored Jul 18, 2022
1 parent 8e5d478 commit ea11b22
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 0 deletions.
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"

0 comments on commit ea11b22

Please sign in to comment.