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.
This commit is contained in:
parent
c31e2a060a
commit
40945f1823
|
@ -348,6 +348,361 @@
|
||||||
"additionalProperties": false
|
"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": {
|
"updates": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
|
@ -41,6 +41,7 @@ snaps:
|
||||||
- name: etcd
|
- name: etcd
|
||||||
channel: 3.2/stable
|
channel: 3.2/stable
|
||||||
updates: all
|
updates: all
|
||||||
|
timezone: Pacific/Guam
|
||||||
storage:
|
storage:
|
||||||
config:
|
config:
|
||||||
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
|
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
|
||||||
|
|
|
@ -55,6 +55,7 @@ python3 scripts/check-yaml-fields.py .subiquity/subiquity-curtin-install.conf \
|
||||||
storage.config[-1].options='"errors=remount-ro"'
|
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) \
|
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"' \
|
locale='"en_GB.UTF-8"' \
|
||||||
|
timezone='"Pacific/Guam"' \
|
||||||
'snap.commands=[snap install --channel=3.2/stable etcd]'
|
'snap.commands=[snap install --channel=3.2/stable etcd]'
|
||||||
grep -q 'finish: subiquity/Install/install/postinstall/install_package1: SUCCESS: installing package1' \
|
grep -q 'finish: subiquity/Install/install/postinstall/install_package1: SUCCESS: installing package1' \
|
||||||
.subiquity/subiquity-server-debug.log
|
.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
|
validate
|
||||||
grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log
|
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"
|
# Limit schema check to Focal+ - the timezone list on bionic is different
|
||||||
diff -u "autoinstall-schema.json" "$testschema"
|
if (( $(echo "$(lsb_release -sr) >= 20.04" | bc -l) )); then
|
||||||
|
python3 -m subiquity.cmd.schema > "$testschema"
|
||||||
|
diff -u "autoinstall-schema.json" "$testschema"
|
||||||
|
fi
|
||||||
|
|
|
@ -37,7 +37,7 @@ class ClickAction(argparse.Action):
|
||||||
|
|
||||||
def make_client_args_parser():
|
def make_client_args_parser():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='SUbiquity - Ubiquity for Servers',
|
description='Subiquity - Ubiquity for Servers',
|
||||||
prog='subiquity')
|
prog='subiquity')
|
||||||
try:
|
try:
|
||||||
ascii_default = os.ttyname(0) == "/dev/ttysclp0"
|
ascii_default = os.ttyname(0) == "/dev/ttysclp0"
|
||||||
|
|
|
@ -42,6 +42,7 @@ from subiquity.common.types import (
|
||||||
SSHData,
|
SSHData,
|
||||||
LiveSessionSSHInfo,
|
LiveSessionSSHInfo,
|
||||||
StorageResponse,
|
StorageResponse,
|
||||||
|
TimeZoneInfo,
|
||||||
WLANSupportInstallState,
|
WLANSupportInstallState,
|
||||||
ZdevInfo,
|
ZdevInfo,
|
||||||
)
|
)
|
||||||
|
@ -226,6 +227,10 @@ class API:
|
||||||
class snap_info:
|
class snap_info:
|
||||||
def GET(snap_name: str) -> SnapInfo: ...
|
def GET(snap_name: str) -> SnapInfo: ...
|
||||||
|
|
||||||
|
class timezone:
|
||||||
|
def GET() -> TimeZoneInfo: ...
|
||||||
|
def POST(tz: str): ...
|
||||||
|
|
||||||
class reboot:
|
class reboot:
|
||||||
def POST(): ...
|
def POST(): ...
|
||||||
|
|
||||||
|
|
|
@ -324,3 +324,9 @@ class SnapListResponse:
|
||||||
status: SnapCheckState
|
status: SnapCheckState
|
||||||
snaps: List[SnapInfo] = attr.Factory(list)
|
snaps: List[SnapInfo] = attr.Factory(list)
|
||||||
selections: List[SnapSelection] = attr.Factory(list)
|
selections: List[SnapSelection] = attr.Factory(list)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class TimeZoneInfo:
|
||||||
|
timezone: str
|
||||||
|
from_geoip: bool
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .network import NetworkModel
|
||||||
from .proxy import ProxyModel
|
from .proxy import ProxyModel
|
||||||
from .snaplist import SnapListModel
|
from .snaplist import SnapListModel
|
||||||
from .ssh import SSHModel
|
from .ssh import SSHModel
|
||||||
|
from .timezone import TimeZoneModel
|
||||||
from .updates import UpdatesModel
|
from .updates import UpdatesModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +85,7 @@ POSTINSTALL_MODEL_NAMES = [
|
||||||
"packages",
|
"packages",
|
||||||
"snaplist",
|
"snaplist",
|
||||||
"ssh",
|
"ssh",
|
||||||
|
"timezone",
|
||||||
"userdata",
|
"userdata",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -121,6 +123,7 @@ class SubiquityModel:
|
||||||
self.proxy = ProxyModel()
|
self.proxy = ProxyModel()
|
||||||
self.snaplist = SnapListModel()
|
self.snaplist = SnapListModel()
|
||||||
self.ssh = SSHModel()
|
self.ssh = SSHModel()
|
||||||
|
self.timezone = TimeZoneModel()
|
||||||
self.updates = UpdatesModel()
|
self.updates = UpdatesModel()
|
||||||
self.userdata = {}
|
self.userdata = {}
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 ("<TimeZone: request {} detect {} should_set {} " +
|
||||||
|
"timezone {} gfg {}>").format(
|
||||||
|
self.request, self.detect_with_geoip, self.should_set_tz,
|
||||||
|
self.timezone, self.got_from_geoip)
|
|
@ -30,6 +30,7 @@ from .refresh import RefreshController
|
||||||
from .reporting import ReportingController
|
from .reporting import ReportingController
|
||||||
from .snaplist import SnapListController
|
from .snaplist import SnapListController
|
||||||
from .ssh import SSHController
|
from .ssh import SSHController
|
||||||
|
from .timezone import TimeZoneController
|
||||||
from .updates import UpdatesController
|
from .updates import UpdatesController
|
||||||
from .userdata import UserdataController
|
from .userdata import UserdataController
|
||||||
from .zdev import ZdevController
|
from .zdev import ZdevController
|
||||||
|
@ -54,6 +55,7 @@ __all__ = [
|
||||||
'ReportingController',
|
'ReportingController',
|
||||||
'SnapListController',
|
'SnapListController',
|
||||||
'SSHController',
|
'SSHController',
|
||||||
|
'TimeZoneController',
|
||||||
'UpdatesController',
|
'UpdatesController',
|
||||||
'UserdataController',
|
'UserdataController',
|
||||||
'ZdevController',
|
'ZdevController',
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
|
@ -196,6 +196,7 @@ class SubiquityServer(Application):
|
||||||
"Identity",
|
"Identity",
|
||||||
"SSH",
|
"SSH",
|
||||||
"SnapList",
|
"SnapList",
|
||||||
|
"TimeZone",
|
||||||
"Install",
|
"Install",
|
||||||
"Updates",
|
"Updates",
|
||||||
"Late",
|
"Late",
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
|
@ -17,4 +17,4 @@ import asyncio
|
||||||
|
|
||||||
|
|
||||||
def run_coro(coro):
|
def run_coro(coro):
|
||||||
asyncio.get_event_loop().run_until_complete(coro)
|
return asyncio.get_event_loop().run_until_complete(coro)
|
||||||
|
|
Loading…
Reference in New Issue