diff --git a/config/main.py b/config/main.py index aa207455af..5f06f59e1e 100644 --- a/config/main.py +++ b/config/main.py @@ -1,6 +1,7 @@ #!/usr/sbin/env python import click +import datetime import ipaddress import json import jsonpatch @@ -7103,5 +7104,67 @@ def del_subinterface(ctx, subinterface_name): except JsonPatchConflict as e: ctx.fail("{} is invalid vlan subinterface. Error: {}".format(subinterface_name, e)) + +# +# 'clock' group ('config clock ...') +# +@config.group() +def clock(): + """Configuring system clock""" + pass + + +def get_tzs(ctx, args, incomplete): + ret = clicommon.run_command('timedatectl list-timezones', + display_cmd=False, ignore_error=False, + return_cmd=True) + if len(ret) == 0: + return [] + + lst = ret[0].split('\n') + return [k for k in lst if incomplete in k] + + +@clock.command() +@click.argument('timezone', metavar='', required=True, + autocompletion=get_tzs) +def timezone(timezone): + """Set system timezone""" + + if timezone not in get_tzs(None, None, ''): + click.echo(f'Timezone {timezone} does not conform format') + sys.exit(1) + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, 'localhost', + {'timezone': timezone}) + + +@clock.command() +@click.argument('date', metavar='', required=True) +@click.argument('time', metavar='', required=True) +def date(date, time): + """Set system date and time""" + valid = True + try: + datetime.datetime.strptime(date, '%Y-%m-%d') + except ValueError: + click.echo(f'Date {date} does not conform format YYYY-MM-DD') + valid = False + + try: + datetime.datetime.strptime(time, '%H:%M:%S') + except ValueError: + click.echo(f'Time {time} does not conform format HH:MM:SS') + valid = False + + if not valid: + sys.exit(1) + + date_time = f'{date} {time}' + clicommon.run_command(['timedatectl', 'set-time', date_time]) + + if __name__ == '__main__': config() diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 03c61f1bd4..ddec476ad6 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -543,6 +543,62 @@ This command displays the current date and time configured on the system Mon Mar 25 20:25:16 UTC 2019 ``` +**config clock date** + +This command will set the date-time of the systetm, given strings with date-time format + +- Usage: + ``` + config clock date + ``` + +- Parameters: + - _date_: valid date in format YYYY-MM-DD + - _time_: valid time in format HH:MM:SS + +- Example: + ``` + admin@sonic:~$ config clock date 2023-04-10 13:54:36 + ``` + +**config clock timezone** + +This command will set the timezone of the systetm, given a string of a valid timezone. + +- Usage: + ``` + config clock timezone + ``` + +- Parameters: + - _timezone_: valid timezone to be configured + + +- Example: + ``` + admin@sonic:~$ config clock timezone Africa/Accra + + +**show clock timezones** + +This command Will display list of all valid timezones to be configured. + +- Usage: + ``` + show clock timezones + ``` + +- Example: + ``` + root@host:~$ show clock timezones + Africa/Abidjan + Africa/Accra + Africa/Addis_Ababa + Africa/Algiers + Africa/Asmara + ... + ``` + **show boot** This command displays the current OS image, the image to be loaded on next reboot, and lists all the available images installed on the device diff --git a/show/main.py b/show/main.py index d79777ebeb..21b284b92b 100755 --- a/show/main.py +++ b/show/main.py @@ -1771,13 +1771,32 @@ def uptime(verbose): cmd = ['uptime', '-p'] run_command(cmd, display_cmd=verbose) -@cli.command() + +# +# 'clock' command group ("show clock ...") +# +@cli.group('clock', invoke_without_command=True) +@click.pass_context @click.option('--verbose', is_flag=True, help="Enable verbose output") -def clock(verbose): +def clock(ctx, verbose): """Show date and time""" - cmd = ["date"] - run_command(cmd, display_cmd=verbose) + # If invoking subcomand, no need to do anything + if ctx.invoked_subcommand is not None: + return + run_command(['date'], display_cmd=verbose) + + +@clock.command() +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def timezones(verbose): + """List of available timezones""" + run_command(['timedatectl', 'list-timezones'], display_cmd=verbose) + + +# +# 'system-memory' command ("show system-memory") +# @cli.command('system-memory') @click.option('--verbose', is_flag=True, help="Enable verbose output") def system_memory(verbose): diff --git a/tests/config_test.py b/tests/config_test.py index b5be1717cb..571800101a 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -2363,3 +2363,65 @@ def test_fec(self, mock_run_command): def teardown(self): print("TEARDOWN") + +class TestConfigClock(object): + timezone_test_val = ['Europe/Kyiv', 'Asia/Israel', 'UTC'] + + @classmethod + def setup_class(cls): + print('SETUP') + import config.main + importlib.reload(config.main) + + @patch('config.main.get_tzs', mock.Mock(return_value=timezone_test_val)) + def test_timezone_good(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke( + config.config.commands['clock'].commands['timezone'], + ['UTC'], obj=obj) + + assert result.exit_code == 0 + + @patch('config.main.get_tzs', mock.Mock(return_value=timezone_test_val)) + def test_timezone_bad(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke( + config.config.commands['clock'].commands['timezone'], + ['Atlantis'], obj=obj) + + assert result.exit_code != 0 + assert 'Timezone Atlantis does not conform format' in result.output + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_date_good(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke( + config.config.commands['clock'].commands['date'], + ['2020-10-10', '10:20:30'], obj=obj) + + assert result.exit_code == 0 + + @patch('utilities_common.cli.run_command', + mock.MagicMock(side_effect=mock_run_command_side_effect)) + def test_date_bad(self): + runner = CliRunner() + obj = {'db': Db().cfgdb} + + result = runner.invoke( + config.config.commands['clock'].commands['date'], + ['20-10-10', '60:70:80'], obj=obj) + + assert result.exit_code != 0 + assert 'Date 20-10-10 does not conform format' in result.output + assert 'Time 60:70:80 does not conform format' in result.output + + @classmethod + def teardown_class(cls): + print('TEARDOWN') diff --git a/tests/show_test.py b/tests/show_test.py index b7f6a9baf8..21af60d8c0 100644 --- a/tests/show_test.py +++ b/tests/show_test.py @@ -926,6 +926,15 @@ def test_show_clock(self, mock_run_command): assert result.exit_code == 0 mock_run_command.assert_called_with(['date'], display_cmd=True) + @patch('show.main.run_command') + def test_show_timezone(self, mock_run_command): + runner = CliRunner() + result = runner.invoke( + show.cli.commands['clock'].commands['timezones'], ['--verbose']) + assert result.exit_code == 0 + mock_run_command.assert_called_once_with( + ['timedatectl', 'list-timezones'], display_cmd=True) + @patch('show.main.run_command') def test_show_system_memory(self, mock_run_command): runner = CliRunner()