Skip to content

Commit ea11b22

Browse files
[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 <[email protected]>
1 parent 8e5d478 commit ea11b22

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

scripts/sonic-bootchart

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
3+
import click
4+
import sys
5+
import configparser
6+
import functools
7+
import os
8+
import glob
9+
from tabulate import tabulate
10+
import utilities_common.cli as clicommon
11+
12+
SYSTEMD_BOOTCHART = "/lib/systemd/systemd-bootchart"
13+
BOOTCHART_CONF = "/etc/systemd/bootchart.conf"
14+
BOOTCHART_DEFAULT_OUTPUT_DIR = "/run/log/"
15+
BOOTCHART_DEFAULT_OUTPUT_GLOB = os.path.join(BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-*.svg")
16+
17+
class BootChartConfigParser(configparser.ConfigParser):
18+
""" Custom bootchart config parser. Changes the way ConfigParser passes options """
19+
20+
def optionxform(self, option):
21+
""" Pass options as is, without modifications """
22+
return option
23+
24+
25+
def exit_cli(*args, **kwargs):
26+
""" Print a message and exit with rc 1. """
27+
click.secho(*args, **kwargs)
28+
sys.exit(1)
29+
30+
31+
def root_privileges_required(func):
32+
""" Decorates a function, so that the function is invoked
33+
only if the user is root. """
34+
@functools.wraps(func)
35+
def wrapped_function(*args, **kwargs):
36+
""" Wrapper around func. """
37+
if os.geteuid() != 0:
38+
exit_cli("Root privileges required for this operation", fg="red")
39+
return func(*args, **kwargs)
40+
41+
wrapped_function.__doc__ += "\n\n NOTE: This command requires elevated (root) privileges to run."
42+
return wrapped_function
43+
44+
45+
def check_bootchart_installed():
46+
""" Fails imidiatelly if bootchart is not installed """
47+
if not os.path.exists(SYSTEMD_BOOTCHART):
48+
exit_cli("systemd-bootchart is not installed", fg="red")
49+
50+
51+
def get_enabled_status():
52+
""" Get systemd-bootchart status """
53+
return clicommon.run_command("systemctl is-enabled systemd-bootchart", return_cmd=True)
54+
55+
def get_active_status():
56+
""" Get systemd-bootchart status """
57+
return clicommon.run_command("systemctl is-active systemd-bootchart", return_cmd=True)
58+
59+
def get_output_files():
60+
bootchart_output_files = []
61+
for bootchart_output_file in glob.glob(BOOTCHART_DEFAULT_OUTPUT_GLOB):
62+
bootchart_output_files.append(bootchart_output_file)
63+
return "\n".join(bootchart_output_files)
64+
65+
66+
@click.group()
67+
def cli():
68+
""" Main CLI group """
69+
check_bootchart_installed()
70+
71+
72+
@cli.command()
73+
@root_privileges_required
74+
def enable():
75+
""" Enable bootchart """
76+
clicommon.run_command("systemctl enable systemd-bootchart", display_cmd=True)
77+
78+
79+
@cli.command()
80+
@root_privileges_required
81+
def disable():
82+
""" Disable bootchart """
83+
clicommon.run_command("systemctl disable systemd-bootchart", display_cmd=True)
84+
85+
86+
@cli.command()
87+
@click.option('--time', type=click.IntRange(min=1), required=True)
88+
@click.option('--frequency', type=click.IntRange(min=1), required=True)
89+
@root_privileges_required
90+
def config(time, frequency):
91+
""" Configure bootchart """
92+
samples = time * frequency
93+
94+
config = {
95+
'Samples': str(samples),
96+
'Frequency': str(frequency),
97+
}
98+
bootchart_config = BootChartConfigParser()
99+
bootchart_config.read(BOOTCHART_CONF)
100+
bootchart_config['Bootchart'].update(config)
101+
with open(BOOTCHART_CONF, 'w') as config_file:
102+
bootchart_config.write(config_file, space_around_delimiters=False)
103+
104+
105+
@cli.command()
106+
def show():
107+
""" Display bootchart configuration """
108+
bootchart_config = BootChartConfigParser()
109+
bootchart_config.read(BOOTCHART_CONF)
110+
111+
try:
112+
samples = int(bootchart_config["Bootchart"]["Samples"])
113+
frequency = int(bootchart_config["Bootchart"]["Frequency"])
114+
except KeyError as key:
115+
raise click.ClickException(f"Failed to parse bootchart config: {key} not found")
116+
except ValueError as err:
117+
raise click.ClickException(f"Failed to parse bootchart config: {err}")
118+
119+
try:
120+
time = samples // frequency
121+
except ZeroDivisionError:
122+
raise click.ClickException(f"Invalid frequency value: {frequency}")
123+
124+
field_values = {
125+
"Status": get_enabled_status(),
126+
"Operational Status": get_active_status(),
127+
"Frequency": frequency,
128+
"Time (sec)": time,
129+
"Output": get_output_files(),
130+
}
131+
132+
click.echo(tabulate([field_values.values()], field_values.keys()))
133+
134+
135+
def main():
136+
cli()
137+
138+
if __name__ == "__main__":
139+
main()

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
'scripts/watermarkstat',
144144
'scripts/watermarkcfg',
145145
'scripts/sonic-kdump-config',
146+
'scripts/sonic-bootchart',
146147
'scripts/centralize_database',
147148
'scripts/null_route_helper',
148149
'scripts/coredump_gen_handler.py',

tests/sonic_bootchart_test.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import os
2+
import subprocess
3+
import pytest
4+
from click.testing import CliRunner
5+
from unittest.mock import patch, Mock
6+
import utilities_common
7+
import imp
8+
9+
sonic_bootchart = imp.load_source('sonic-bootchart', 'scripts/sonic-bootchart')
10+
11+
BOOTCHART_OUTPUT_FILES = [
12+
os.path.join(sonic_bootchart.BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-20220504-1040.svg"),
13+
os.path.join(sonic_bootchart.BOOTCHART_DEFAULT_OUTPUT_DIR, "bootchart-20220504-1045.svg"),
14+
]
15+
16+
@pytest.fixture(autouse=True)
17+
def setup(fs):
18+
# create required file for bootchart installation check
19+
fs.create_file(sonic_bootchart.SYSTEMD_BOOTCHART)
20+
fs.create_file(sonic_bootchart.BOOTCHART_CONF)
21+
for bootchart_output_file in BOOTCHART_OUTPUT_FILES:
22+
fs.create_file(bootchart_output_file)
23+
24+
with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
25+
config_file.write("""
26+
[Bootchart]
27+
Samples=500
28+
Frequency=25
29+
""")
30+
31+
# pass the root user check
32+
with patch("os.geteuid") as mock:
33+
mock.return_value = 0
34+
yield
35+
36+
37+
@patch("utilities_common.cli.run_command")
38+
class TestSonicBootchart:
39+
def test_enable(self, mock_run_command):
40+
runner = CliRunner()
41+
result = runner.invoke(sonic_bootchart.cli.commands['enable'], [])
42+
assert not result.exit_code
43+
mock_run_command.assert_called_with("systemctl enable systemd-bootchart", display_cmd=True)
44+
45+
def test_disable(self, mock_run_command):
46+
runner = CliRunner()
47+
result = runner.invoke(sonic_bootchart.cli.commands['disable'], [])
48+
assert not result.exit_code
49+
mock_run_command.assert_called_with("systemctl disable systemd-bootchart", display_cmd=True)
50+
51+
def test_config_show(self, mock_run_command):
52+
def run_command_side_effect(command, **kwargs):
53+
if "is-enabled" in command:
54+
return "enabled"
55+
elif "is-active" in command:
56+
return "active"
57+
else:
58+
raise Exception("unknown command")
59+
60+
mock_run_command.side_effect = run_command_side_effect
61+
62+
runner = CliRunner()
63+
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
64+
assert not result.exit_code
65+
assert result.output == \
66+
"Status Operational Status Frequency Time (sec) Output\n" \
67+
"-------- -------------------- ----------- ------------ ------------------------------------\n" \
68+
"enabled active 25 20 /run/log/bootchart-20220504-1040.svg\n" \
69+
" /run/log/bootchart-20220504-1045.svg\n"
70+
71+
result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "2", "--frequency", "50"])
72+
assert not result.exit_code
73+
74+
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
75+
assert not result.exit_code
76+
assert result.output == \
77+
"Status Operational Status Frequency Time (sec) Output\n" \
78+
"-------- -------------------- ----------- ------------ ------------------------------------\n" \
79+
"enabled active 50 2 /run/log/bootchart-20220504-1040.svg\n" \
80+
" /run/log/bootchart-20220504-1045.svg\n"
81+
82+
# Input validation tests
83+
84+
result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "0", "--frequency", "50"])
85+
assert result.exit_code
86+
87+
result = runner.invoke(sonic_bootchart.cli.commands["config"], ["--time", "2", "--frequency", "-5"])
88+
assert result.exit_code
89+
90+
def test_invalid_config_show(self, mock_run_command):
91+
with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
92+
config_file.write("""
93+
[Bootchart]
94+
Samples=100
95+
""")
96+
97+
runner = CliRunner()
98+
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
99+
assert result.exit_code
100+
assert result.output == "Error: Failed to parse bootchart config: 'Frequency' not found\n"
101+
102+
with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
103+
config_file.write("""
104+
[Bootchart]
105+
Samples=abc
106+
Frequency=def
107+
""")
108+
109+
runner = CliRunner()
110+
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
111+
assert result.exit_code
112+
assert result.output == "Error: Failed to parse bootchart config: invalid literal for int() with base 10: 'abc'\n"
113+
114+
with open(sonic_bootchart.BOOTCHART_CONF, 'w') as config_file:
115+
config_file.write("""
116+
[Bootchart]
117+
Samples=100
118+
Frequency=0
119+
""")
120+
121+
runner = CliRunner()
122+
result = runner.invoke(sonic_bootchart.cli.commands['show'], [])
123+
assert result.exit_code
124+
assert result.output == "Error: Invalid frequency value: 0\n"

0 commit comments

Comments
 (0)