-
Notifications
You must be signed in to change notification settings - Fork 666
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[sonic-bootchart] add sonic-bootchart (#2195)
- 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
1 parent
8e5d478
commit ea11b22
Showing
3 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |