diff --git a/config/main.py b/config/main.py index 9a71f1078b..d9bb50569a 100644 --- a/config/main.py +++ b/config/main.py @@ -8217,5 +8217,82 @@ def motd(message): {'motd': message}) +# +# 'logrotate' group ('config logrotate ...') +# +@config.group(invoke_without_command=True) +@click.pass_context +@click.argument('file', required=True, type=click.Choice(['syslog', 'debug'])) +def logrotate(ctx, file): + """Configuring logrotate""" + # If invoking subcomand, no need to do anything + if ctx.invoked_subcommand is not None: + return + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_LOGGING_TABLE_NAME, file, None) + + +@logrotate.command() +@click.pass_context +@click.argument('disk_percentage', metavar='', + required=True, type=float) +def disk_percentage(ctx, disk_percentage): + """Configuring logrotate disk-precentage""" + file = ctx.parent.params.get('file') + if disk_percentage <= 0 or disk_percentage > 100: + click.echo(f'Disk percentage {disk_percentage} is not in range (0 - 100]') + sys.exit(1) + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_LOGGING_TABLE_NAME, file, + {'disk_percentage': disk_percentage}) + + +@logrotate.command(name='frequency') +@click.pass_context +@click.argument('frequency', metavar='', + required=True, + type=click.Choice(['daily', 'weekly', 'monthly', 'yearly'])) +def logrotate_frequency(ctx, frequency): + """Configuring logrotate rotation frequency""" + file = ctx.parent.params.get('file') + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_LOGGING_TABLE_NAME, file, + {'frequency': frequency}) + + +@logrotate.command() +@click.pass_context +@click.argument('max_number', metavar='', + type=click.IntRange(0, 999999), required=True) +def max_number(ctx, max_number): + """Configuring logrotate max-number of files""" + file = ctx.parent.params.get('file') + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_LOGGING_TABLE_NAME, file, + {'max_number': max_number}) + + +@logrotate.command(name='size') +@click.pass_context +@click.argument('size', metavar='', type=float, required=True) +def logrotate_size(ctx, size): + """Configuring logrotate size of file""" + file = ctx.parent.params.get('file') + if size < 0.001 or size > 3500.0: + click.echo(f'Size {size} is not in range [0.001 - 3500.0]') + sys.exit(1) + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_LOGGING_TABLE_NAME, file, + {'size': size}) + + if __name__ == '__main__': config() diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index c6c6f2003a..f0294e6c30 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -112,7 +112,10 @@ * [Reloading Configuration](#reloading-configuration) * [Loading Management Configuration](#loading-management-configuration) * [Saving Configuration to a File for Persistence](#saving-configuration-to-a-file-for-persistence) - * [Loopback Interfaces](#loopback-interfaces) +* [Logrotate](#logrotate) + * [Logrotate config commands](#logrotate-config-commands) + * [Logrotate show command](#logrotate-show-command) +* [Loopback Interfaces](#loopback-interfaces) * [Loopback show commands](#loopback-show-commands) * [Loopback config commands](#loopback-config-commands) * [VRF Configuration](#vrf-configuration) @@ -13907,4 +13910,67 @@ enabled Login You are on All access and/or use are subject to monitoring. Help: https://sonic-net.github.io/SONiC/ +``` + +# Logrotate Commands +This sub-section explains the list of the configuration options available for logrotate feature. +## Logrotate config commands +- Rotate logs when they surpass a specified percentage of disk +``` +admin@sonic:~$ config logrotate disk-percentage <> <> +Usage: config logrotate disk-precentage [OPTIONS] + + Configuring logrotate disk-precentage file + +Options: + -?, -h, --help Show this message and exit. +``` +- Log files rotation frequency +``` +admin@sonic:~$ config logrotate frequency +Usage: config logrotate frequency [OPTIONS] + + Configuring logrotate frequency file + +Options: + -?, -h, --help Show this message and exit. +``` +- Max number of log files to keep +``` +admin@sonic:~$ config logrotate max-number +Usage: config logrotate max-number [OPTIONS] + + Configuring logrotate max-number file + +Options: + -?, -h, --help Show this message and exit. +``` +- Rotate logs if they grow bigger then size in Mebibytes +``` +admin@sonic:~$ config logrotate size +Usage: config logrotate size [OPTIONS] + + Configuring logrotate size file + +Options: + -h, -?, --help Show this message and exit. +``` +## Logrotate show command +- Show logrotate configuration +``` +admin@sonic:~$ show logrotate +Usage: show logrotate [OPTIONS] COMMAND [ARGS]... + + Show logrotate configuration + +Options: + -?, -h, --help Show this message and exit. +``` +``` +admin@sonic:~$ show logrotate +file disk-percentage frequency max-number size +------ ----------------- ----------- ------------ ------ +syslog 10.2 daily 10 20.0 +debug 50.5 weekly 20 10.0 + ``` \ No newline at end of file diff --git a/show/main.py b/show/main.py index 3151e4d61b..8cc9cd07bf 100755 --- a/show/main.py +++ b/show/main.py @@ -2636,6 +2636,29 @@ def banner(db): click.echo(tabulate(messages, headers=hdrs, tablefmt='simple', missingval='')) +# +# 'logrorare' command group ("show logrotate ...") +# +@cli.group('logrotate', invoke_without_command=True) +@clicommon.pass_db +def logrotate(db): + """Show logrotate configuration""" + + table = [] + + logging_table = db.cfgdb.get_table('LOGGING') + for key, value in logging_table.items(): + disk_percentage = value.get('disk_percentage', '') + frequency = value.get('frequency', '') + max_number = value.get('max_number', '') + size = value.get('size', '') + + table.append((key, disk_percentage, frequency, max_number, size)) + + hdrs = ['file', 'disk-percentage', 'frequency', 'max-number', 'size (MiB)'] + click.echo(tabulate(table, headers=hdrs, tablefmt='simple', missingval='')) + + # Load plugins and register them helper = util_base.UtilHelper() helper.load_and_register_plugins(plugins, cli) diff --git a/tests/config_test.py b/tests/config_test.py index 1809b5545d..be549edb07 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -4013,3 +4013,75 @@ def test_banner_motd(self): @classmethod def teardown_class(cls): print('TEARDOWN') + + +class TestConfigLogrotate(object): + @classmethod + def setup_class(cls): + print('SETUP') + import config.main + importlib.reload(config.main) + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_disk_percentage(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['debug', 'disk-percentage', '24.25'], obj=obj) + assert result.exit_code == 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_disk_percentage_invalid(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['debug', 'disk-percentage', '150'], obj=obj) + assert result.exit_code != 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_frequency(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['debug', 'frequency', 'daily'], obj=obj) + assert result.exit_code == 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_max_number(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['syslog', 'max-number', '2024'], obj=obj) + assert result.exit_code == 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_size(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['syslog', 'size', '30.0'], obj=obj) + assert result.exit_code == 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_logrotate_size_invalid(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke(config.config.commands['logrotate'], + ['syslog', 'size', '10000'], obj=obj) + assert result.exit_code != 0 + + @classmethod + def teardown_class(cls): + print('TEARDOWN') diff --git a/tests/show_test.py b/tests/show_test.py index 819f197343..93d236b3aa 100644 --- a/tests/show_test.py +++ b/tests/show_test.py @@ -1046,6 +1046,12 @@ def test_show_banner(self, mock_run_command): result = runner.invoke(show.cli.commands['banner']) assert result.exit_code == 0 + @patch('show.main.run_command') + def test_show_logrotate(self, mock_run_command): + runner = CliRunner() + result = runner.invoke(show.cli.commands['logrotate']) + assert result.exit_code == 0 + def teardown(self): print('TEAR DOWN')