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

[sonic-bootchart] add sonic-bootchart #2195

Merged
merged 8 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from 7 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
130 changes: 130 additions & 0 deletions scripts/sonic-bootchart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/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)

samples = int(bootchart_config["Bootchart"]["Samples"])
frequency = int(bootchart_config["Bootchart"]["Frequency"])
time = samples // frequency
Copy link
Contributor

@qiluo-msft qiluo-msft Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frequency

Check input for zero? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And add testcase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qiluo-msft Added Config parsing error handling (expected key not found, non integer value and zero frequency) and corresponding unit tests


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
88 changes: 88 additions & 0 deletions tests/sonic_bootchart_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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