From 40945f1823248a2d630e23faf631e80560a6e494 Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Tue, 13 Jul 2021 06:25:03 -0600 Subject: [PATCH] TimeZone: autoinstall and API (FR-1184) (#986) * TimeZone: autoinstall and API Add support for Get/Set timezone methods. Get means that we inquire with GeoIP as to which timezone is suggested. Non-availability of GeoIP, or a previous explicit Set, means that we return the system timezone. Set of timezone by Post results in set of the live system timzeone, and queuing a set of the target system by way of cloud-init. * Add clarifying comment about _request. --- autoinstall-schema.json | 355 +++++++++++++++++++++ examples/autoinstall.yaml | 1 + scripts/runtests.sh | 8 +- subiquity/cmd/tui.py | 2 +- subiquity/common/apidef.py | 5 + subiquity/common/types.py | 6 + subiquity/models/subiquity.py | 3 + subiquity/models/timezone.py | 63 ++++ subiquity/server/controllers/__init__.py | 2 + subiquity/server/controllers/timezone.py | 120 +++++++ subiquity/server/server.py | 1 + subiquity/tests/test_timezonecontroller.py | 95 ++++++ subiquitycore/tests/util.py | 2 +- 13 files changed, 659 insertions(+), 4 deletions(-) create mode 100644 subiquity/models/timezone.py create mode 100644 subiquity/server/controllers/timezone.py create mode 100644 subiquity/tests/test_timezonecontroller.py diff --git a/autoinstall-schema.json b/autoinstall-schema.json index 1eeb92c6..1e78394d 100644 --- a/autoinstall-schema.json +++ b/autoinstall-schema.json @@ -348,6 +348,361 @@ "additionalProperties": false } }, + "timezone": { + "type": "string", + "enum": [ + "", + "geoip", + "Africa/Abidjan", + "Africa/Accra", + "Africa/Algiers", + "Africa/Bissau", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/El_Aaiun", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Khartoum", + "Africa/Lagos", + "Africa/Maputo", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Johns", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Helsinki", + "Europe/Istanbul", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zaporozhye", + "Europe/Zurich", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "UTC" + ] + }, "updates": { "type": "string", "enum": [ diff --git a/examples/autoinstall.yaml b/examples/autoinstall.yaml index 65252f0b..51aff9e2 100644 --- a/examples/autoinstall.yaml +++ b/examples/autoinstall.yaml @@ -41,6 +41,7 @@ snaps: - name: etcd channel: 3.2/stable updates: all +timezone: Pacific/Guam storage: config: - {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1} diff --git a/scripts/runtests.sh b/scripts/runtests.sh index 28785ad0..2f376a1f 100755 --- a/scripts/runtests.sh +++ b/scripts/runtests.sh @@ -55,6 +55,7 @@ python3 scripts/check-yaml-fields.py .subiquity/subiquity-curtin-install.conf \ storage.config[-1].options='"errors=remount-ro"' python3 scripts/check-yaml-fields.py <(python3 scripts/check-yaml-fields.py .subiquity/etc/cloud/cloud.cfg.d/99-installer.cfg datasource.None.userdata_raw) \ locale='"en_GB.UTF-8"' \ + timezone='"Pacific/Guam"' \ 'snap.commands=[snap install --channel=3.2/stable etcd]' grep -q 'finish: subiquity/Install/install/postinstall/install_package1: SUCCESS: installing package1' \ .subiquity/subiquity-server-debug.log @@ -70,5 +71,8 @@ timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --autoi validate grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log -python3 -m subiquity.cmd.schema > "$testschema" -diff -u "autoinstall-schema.json" "$testschema" +# Limit schema check to Focal+ - the timezone list on bionic is different +if (( $(echo "$(lsb_release -sr) >= 20.04" | bc -l) )); then + python3 -m subiquity.cmd.schema > "$testschema" + diff -u "autoinstall-schema.json" "$testschema" +fi diff --git a/subiquity/cmd/tui.py b/subiquity/cmd/tui.py index 4417d6f3..8bf6901d 100755 --- a/subiquity/cmd/tui.py +++ b/subiquity/cmd/tui.py @@ -37,7 +37,7 @@ class ClickAction(argparse.Action): def make_client_args_parser(): parser = argparse.ArgumentParser( - description='SUbiquity - Ubiquity for Servers', + description='Subiquity - Ubiquity for Servers', prog='subiquity') try: ascii_default = os.ttyname(0) == "/dev/ttysclp0" diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 122355c8..3136a1df 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -42,6 +42,7 @@ from subiquity.common.types import ( SSHData, LiveSessionSSHInfo, StorageResponse, + TimeZoneInfo, WLANSupportInstallState, ZdevInfo, ) @@ -226,6 +227,10 @@ class API: class snap_info: def GET(snap_name: str) -> SnapInfo: ... + class timezone: + def GET() -> TimeZoneInfo: ... + def POST(tz: str): ... + class reboot: def POST(): ... diff --git a/subiquity/common/types.py b/subiquity/common/types.py index e4850a92..6bbc6250 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -324,3 +324,9 @@ class SnapListResponse: status: SnapCheckState snaps: List[SnapInfo] = attr.Factory(list) selections: List[SnapSelection] = attr.Factory(list) + + +@attr.s(auto_attribs=True) +class TimeZoneInfo: + timezone: str + from_geoip: bool diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index 8750a2eb..8303fe48 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -38,6 +38,7 @@ from .network import NetworkModel from .proxy import ProxyModel from .snaplist import SnapListModel from .ssh import SSHModel +from .timezone import TimeZoneModel from .updates import UpdatesModel @@ -84,6 +85,7 @@ POSTINSTALL_MODEL_NAMES = [ "packages", "snaplist", "ssh", + "timezone", "userdata", ] @@ -121,6 +123,7 @@ class SubiquityModel: self.proxy = ProxyModel() self.snaplist = SnapListModel() self.ssh = SSHModel() + self.timezone = TimeZoneModel() self.updates = UpdatesModel() self.userdata = {} diff --git a/subiquity/models/timezone.py b/subiquity/models/timezone.py new file mode 100644 index 00000000..433ae625 --- /dev/null +++ b/subiquity/models/timezone.py @@ -0,0 +1,63 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +log = logging.getLogger('subiquity.models.timezone') + + +class TimeZoneModel(object): + """ Model representing timezone""" + + timezone = '' + + def __init__(self): + # This is the raw request from the API / autoinstall. + # Storing the raw request allows us to also report the requested + # value in /var/log/installer/autoinstall-user-data, + # so that if 'geoip' is requested, and multiple installs are + # done in different zones, they get a geoip-customized answer. + self._request = None + # The actually timezone to set, possibly post-geoip lookup or + # possibly manually specified. + self.timezone = '' + self.got_from_geoip = False + + def set(self, value): + self._request = value + self.timezone = value if value != 'geoip' else '' + + @property + def detect_with_geoip(self): + return self._request == 'geoip' + + @property + def should_set_tz(self): + return bool(self.timezone) + + @property + def request(self): + return self._request + + def make_cloudconfig(self): + if not self.should_set_tz: + return {} + return {'timezone': self.timezone} + + def __repr__(self): + return ("").format( + self.request, self.detect_with_geoip, self.should_set_tz, + self.timezone, self.got_from_geoip) diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py index 2a6c54d6..a4498c11 100644 --- a/subiquity/server/controllers/__init__.py +++ b/subiquity/server/controllers/__init__.py @@ -30,6 +30,7 @@ from .refresh import RefreshController from .reporting import ReportingController from .snaplist import SnapListController from .ssh import SSHController +from .timezone import TimeZoneController from .updates import UpdatesController from .userdata import UserdataController from .zdev import ZdevController @@ -54,6 +55,7 @@ __all__ = [ 'ReportingController', 'SnapListController', 'SSHController', + 'TimeZoneController', 'UpdatesController', 'UserdataController', 'ZdevController', diff --git a/subiquity/server/controllers/timezone.py b/subiquity/server/controllers/timezone.py new file mode 100644 index 00000000..332e5e58 --- /dev/null +++ b/subiquity/server/controllers/timezone.py @@ -0,0 +1,120 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging +import subprocess + +from subiquity.common.apidef import API +from subiquity.common.types import TimeZoneInfo +from subiquity.server.controller import SubiquityController + +log = logging.getLogger('subiquity.server.controllers.timezone') + + +def generate_possible_tzs(): + special_keys = ['', 'geoip'] + tzcmd = ['timedatectl', 'list-timezones'] + list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True) + real_tzs = list_tz_out.splitlines() + return special_keys + real_tzs + + +def timedatectl_settz(tz): + tzcmd = ['timedatectl', 'set-timezone', tz] + try: + subprocess.run(tzcmd, universal_newlines=True) + except subprocess.CalledProcessError as cpe: + log.error('Failed to set live system timezone: %r', cpe) + + +def timedatectl_gettz(): + # timedatectl show would be easier, but isn't on bionic + tzcmd = ['timedatectl', 'status'] + env = {'LC_ALL': 'C'} + # ... + # Time zone: America/Denver (MDT, -0600) + # ... + try: + out = subprocess.check_output(tzcmd, env=env, universal_newlines=True) + for line in out.splitlines(): + chunks = line.split(':') + label = chunks[0].strip() + if label != 'Time zone': + continue + chunks = chunks[1].split(' ') + return chunks[1] + except subprocess.CalledProcessError as cpe: + log.error('Failed to get live system timezone: %r', cpe) + except IndexError: + log.error('Failed to acquire system time zone') + log.debug('Failed to fine Time zone in timedatectl output') + return 'Etc/UTC' + + +class TimeZoneController(SubiquityController): + + endpoint = API.timezone + + possible = generate_possible_tzs() + + autoinstall_key = model_name = 'timezone' + autoinstall_schema = { + 'type': 'string', + 'enum': possible + } + + autoinstall_default = '' + relevant_variants = ('desktop', ) + + def load_autoinstall_data(self, data): + self.deserialize(data) + + def make_autoinstall(self): + return self.serialize() + + def serialize(self): + return self.model.request + + def deserialize(self, data): + if data is None: + return + if data not in self.possible: + raise ValueError(f'Unrecognized time zone request "{data}"') + self.model.set(data) + if self.model.detect_with_geoip and self.app.geoip.timezone: + self.model.timezone = self.app.geoip.timezone + self.model.got_from_geoip = True + else: + self.model.got_from_geoip = False + self.set_system_timezone() + + def set_system_timezone(self): + if self.model.should_set_tz: + timedatectl_settz(self.model.timezone) + + async def GET(self) -> TimeZoneInfo: + if self.model.timezone: + return TimeZoneInfo(self.model.timezone, + self.model.got_from_geoip) + + # a bare call to GET() is equivalent to autoinstall "timezone: geoip" + self.deserialize('geoip') + tz = self.model.timezone + if not tz: + tz = timedatectl_gettz() + return TimeZoneInfo(tz, self.model.got_from_geoip) + + async def POST(self, tz: str): + self.deserialize(tz) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 50b8f843..b433ff10 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -196,6 +196,7 @@ class SubiquityServer(Application): "Identity", "SSH", "SnapList", + "TimeZone", "Install", "Updates", "Late", diff --git a/subiquity/tests/test_timezonecontroller.py b/subiquity/tests/test_timezonecontroller.py new file mode 100644 index 00000000..fff2ba53 --- /dev/null +++ b/subiquity/tests/test_timezonecontroller.py @@ -0,0 +1,95 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import mock + +from subiquity.common.types import TimeZoneInfo +from subiquity.models.timezone import TimeZoneModel +from subiquity.server.controllers.timezone import TimeZoneController +from subiquitycore.tests import SubiTestCase +from subiquitycore.tests.mocks import make_app +from subiquitycore.tests.util import run_coro + + +class MockGeoIP: + text = '' + + @property + def timezone(self): + return self.text + + +tz_denver = 'America/Denver' +tz_utc = 'Etc/UTC' + + +class TestTimeZoneController(SubiTestCase): + def setUp(self): + self.tzc_init() + + def tzc_init(self): + self.tzc = TimeZoneController(make_app()) + self.tzc.model = TimeZoneModel() + self.tzc.app.geoip = MockGeoIP() + self.tzc.app.geoip.text = tz_denver + + @mock.patch('subiquity.server.controllers.timezone.timedatectl_settz') + @mock.patch('subiquity.server.controllers.timezone.timedatectl_gettz') + def test_good_tzs(self, tdc_gettz, tdc_settz): + tdc_gettz.return_value = tz_utc + goods = [ + # val - autoinstall value + # | settz - should system set timezone + # | | geoip - did we get a value by geoip? + # | | | valid_lookup + ('geoip', True, True, True), + ('geoip', False, True, False), + # empty val is valid and means to set no time zone + ('', False, False, True), + ('Pacific/Auckland', True, False, False), + ('America/Denver', True, False, False), + ] + for val, settz, geoip, valid_lookup in goods: + self.tzc_init() + if not val or val == 'geoip': + if valid_lookup: + tz = TimeZoneInfo(tz_denver, True) + else: + tz = TimeZoneInfo(tz_utc, False) + self.tzc.app.geoip.text = '' + else: + tz = TimeZoneInfo(val, False) + self.tzc.deserialize(val) + self.assertEqual(val, self.tzc.serialize()) + self.assertEqual(settz, self.tzc.model.should_set_tz, + self.tzc.model) + self.assertEqual(geoip, self.tzc.model.detect_with_geoip, + self.tzc.model) + self.assertEqual(tz, run_coro(self.tzc.GET()), self.tzc.model) + cloudconfig = {} + if self.tzc.model.should_set_tz: + cloudconfig = {'timezone': tz.timezone} + tdc_settz.assert_called_with(tz.timezone) + self.assertEqual(cloudconfig, self.tzc.model.make_cloudconfig(), + self.tzc.model) + + def test_bad_tzs(self): + bads = [ + 'dhcp', # possible future value, not supported yet + 'notatimezone', + ] + for b in bads: + with self.assertRaises(ValueError): + self.tzc.deserialize(b) diff --git a/subiquitycore/tests/util.py b/subiquitycore/tests/util.py index ab270b23..f69f6bc0 100644 --- a/subiquitycore/tests/util.py +++ b/subiquitycore/tests/util.py @@ -17,4 +17,4 @@ import asyncio def run_coro(coro): - asyncio.get_event_loop().run_until_complete(coro) + return asyncio.get_event_loop().run_until_complete(coro)