Skip to content

Make timezone parsing more portable and robust #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 56 additions & 19 deletions mig/shared/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,61 @@
from mig.shared.url import urlparse


def determine_timezone(_environ=os.environ, _path_exists=os.path.exists, _print=print):
"""Attempt to detect the timezone in various known portable ways."""

sys_timezone = None

timezone_link = '/etc/localtime'
timezone_cmd = ["/usr/bin/timedatectl", "status"]

env_timezone = _environ.get('TZ', None)
if env_timezone:
# Use TZ env value directly if set
return env_timezone

if _path_exists(timezone_link):
zoneinfo_absolute = os.path.realpath(timezone_link)
# Convert /etc/localtime link to e.g. /.../zoneinfo/Europe/Rome
# then remove leading directories leaving TZ e.g. Europe/Rome
zoneinfo_path_parts = zoneinfo_absolute.split('/')
try:
zoneinfo_index = zoneinfo_path_parts.index('zoneinfo')
# the last path parts are at least .../zoneinfo/ which
# is good enough for us here - treat them as the timezone
localtime_timezone = '/'.join(
zoneinfo_path_parts[zoneinfo_index + 1:])
return localtime_timezone
except IndexError:
pass

_print("WARNING: ignoring non-standard /etc/localtime")

if _path_exists(timezone_cmd[0]):
# Parse Time zone: LOCATION (ALIAS, OFFSET) output of timedatectl
# into just LOCATION
try:
timezone_proc = subprocess_popen(
timezone_cmd, stdout=subprocess_pipe)
for line in timezone_proc.stdout.readlines():
line = ensure_native_string(line.strip())
if not line.startswith("Time zone: "):
continue
timedatectl_parts = line.replace(
"Time zone: ", "").split(" ", 1)
return timedatectl_parts[0]
except IndexError:
pass
except OSError as exc:
# warn about any issues executing the command but continue
_print("WARNING: failed to extract time zone with %s : %s" %
(' '.join(timezone_cmd), exc))

# none of the standard extraction methods succeeded by this point
_print("WARNING: failed to extract system time zone; defaulting to UTC")
return 'UTC'


def fill_template(template_file, output_file, settings, eat_trailing_space=[],
additional=None):
"""Fill a configuration template using provided settings dictionary"""
Expand Down Expand Up @@ -944,25 +999,7 @@ def generate_confs(
user_dict['__DUPLICATI_PROTOCOLS__'] = ' '.join(prio_duplicati_protocols)

if timezone == keyword_auto:
# attempt to detect the timezone
sys_timezone = None
try:
timezone_cmd = ["/usr/bin/timedatectl", "status"]
timezone_proc = subprocess_popen(
timezone_cmd, stdout=subprocess_pipe)
for line in timezone_proc.stdout.readlines():
line = ensure_native_string(line.strip())
if not line.startswith("Time zone: "):
continue
sys_timezone = line.replace("Time zone: ", "").split(" ", 1)[0]
except OSError as exc:
# warn about any issues executing the command but continue
pass
if sys_timezone is None:
print("WARNING: failed to extract system time zone; defaulting to UTC")
sys_timezone = 'UTC'

timezone = sys_timezone
timezone = determine_timezone()

user_dict['__SEAFILE_TIMEZONE__'] = timezone

Expand Down
51 changes: 50 additions & 1 deletion tests/test_mig_shared_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@

from support import MigTestCase, testmain, temppath, cleanpath, fixturepath

from mig.shared.install import generate_confs
from mig.shared.install import determine_timezone, generate_confs


class DummyPwInfo:
"""Wrapper to assist in create_dummy_gpwnam"""
Expand All @@ -54,6 +55,54 @@ def create_dummy_gpwnam(pw_uid, pw_gid):
return lambda _: dummy


def noop(*args, **kwargs):
pass


class MigSharedInstall__determine_timezone(MigTestCase):
"""Coverage of timezone determination."""

def test_determines_tz_utc_fallback(self):
timezone = determine_timezone(
_environ={}, _path_exists=lambda _: False, _print=noop)

self.assertEqual(timezone, 'UTC')

def test_determines_tz_via_environ(self):
example_environ = {
'TZ': 'Example/Enviromnent'
}
timezone = determine_timezone(_environ=example_environ)

self.assertEqual(timezone, 'Example/Enviromnent')

def test_determines_tz_via_localtime(self):
def exists_localtime(value):
saw_call = value == '/etc/localtime'
exists_localtime.was_called = saw_call
return saw_call
exists_localtime.was_called = False

timezone = determine_timezone(
_environ={}, _path_exists=exists_localtime)

self.assertTrue(exists_localtime.was_called)
self.assertIsNotNone(timezone)

def test_determines_tz_via_timedatectl(self):
def exists_timedatectl(value):
saw_call = value == '/usr/bin/timedatectl'
exists_timedatectl.was_called = saw_call
return saw_call
exists_timedatectl.was_called = False

timezone = determine_timezone(
_environ={}, _path_exists=exists_timedatectl, _print=noop)

self.assertTrue(exists_timedatectl.was_called)
self.assertIsNotNone(timezone)


class MigSharedInstall__generate_confs(MigTestCase):
"""Unit test helper for the migrid code pointed to in class name"""

Expand Down
Loading