Skip to content

Commit

Permalink
Try to resolve locale before running Click (#32)
Browse files Browse the repository at this point in the history
* Remove .python-version

* Format and lint only sources and tests

* Add LocaleResolver to fix LC_ALL & LANG issues

* Use LocaleResolver in chadmin

* Use LocaleResolver in monrun_checks

* Use LocaleResolver in monrun_checks_keeper

* Remove usage of deprecated `locale.getdefaultlocale()`
dstaroff authored Aug 3, 2023
1 parent 46faefc commit 20c47d8
Showing 6 changed files with 182 additions and 29 deletions.
1 change: 0 additions & 1 deletion .python-version

This file was deleted.

8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -136,11 +136,11 @@ lint: isort black flake8 pylint mypy

.PHONY: isort
isort:
isort --check --diff .
isort --check --diff src tests

.PHONY: black
black:
black --check --diff .
black --check --diff src tests

.PHONY: flake8
flake8:
@@ -157,8 +157,8 @@ mypy:

.PHONY: format
format:
isort .
black .
isort src tests
black src tests


.PHONY: help
8 changes: 8 additions & 0 deletions src/ch_tools/chadmin/chadmin_cli.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

import cloup

from ch_tools import __version__
from ch_tools.chadmin.cli.chs3_backup_group import chs3_backup_group
from ch_tools.chadmin.cli.config_command import config_command
from ch_tools.chadmin.cli.crash_log_group import crash_log_group
@@ -42,6 +43,7 @@
from ch_tools.chadmin.cli.wait_started_command import wait_started_command
from ch_tools.chadmin.cli.zookeeper_group import zookeeper_group
from ch_tools.common.cli.context_settings import CONTEXT_SETTINGS
from ch_tools.common.cli.locale_resolver import LocaleResolver
from ch_tools.common.cli.parameters import TimeSpanParamType

LOG_FILE = "/var/log/chadmin/chadmin.log"
@@ -71,6 +73,7 @@
@cloup.option("--timeout", type=TimeSpanParamType(), help="Timeout for SQL queries.")
@cloup.option("--port", type=int, help="Port to connect.")
@cloup.option("-d", "--debug", is_flag=True, help="Enable debug output.")
@cloup.version_option(__version__)
@cloup.pass_context
def cli(ctx, format_, settings, timeout, port, debug):
"""ClickHouse administration tool."""
@@ -147,4 +150,9 @@ def main():
"""
Program entry point.
"""
LocaleResolver.resolve()
cli.main()


if __name__ == "__main__":
main()
81 changes: 81 additions & 0 deletions src/ch_tools/common/cli/locale_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import locale
import os
import subprocess
from typing import List, Tuple

__all__ = [
"LocaleResolver",
]


class LocaleResolver:
"""
Sets the locale for Click. Otherwise, it may fail with an error like
```
RuntimeError: Click discovered that you exported a UTF-8 locale
but the locale system could not pick up from it because it does not exist.
The exported locale is 'en_US.UTF-8' but it is not supported.
```
"""

@staticmethod
def resolve():
lang, _ = locale.getlocale()
locales, has_c, has_en_us = LocaleResolver._get_utf8_locales()

langs = map(lambda loc: str.lower(loc[0]), locales)
if lang is None or lang.lower() not in langs:
if has_c:
lang = "C"
elif has_en_us:
lang = "en_US"
else:
raise RuntimeError(
f'Locale "{lang}" is not supported. '
'We tried to use "C" and "en_US" but they\'re absent on your machine.',
)

for locale_ in locales:
if lang != locale_[0]:
continue

os.environ["LC_ALL"] = f"{lang}.{locale_[1]}"
os.environ["LANG"] = f"{lang}.{locale_[1]}"

@staticmethod
def _get_utf8_locales() -> Tuple[List[Tuple[str, str]], bool, bool]:
try:
with subprocess.Popen(
["locale", "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="ascii",
errors="replace",
) as proc:
stdout, _ = proc.communicate()
except OSError:
stdout = ""

langs = []
encodings = []

has_c = False
has_en_us = False

for line in stdout.splitlines():
locale_ = line.strip()
if not locale_.lower().endswith(("utf-8", "utf8")):
continue

lang, encoding = locale_.split(".")

langs.append(lang)
encodings.append(encoding)

has_c |= lang.lower() == "c"
has_en_us |= lang.lower() == "en_us"

res = list(zip(langs, encodings))

return res, has_c, has_en_us
53 changes: 41 additions & 12 deletions src/ch_tools/monrun_checks/main.py
Original file line number Diff line number Diff line change
@@ -5,13 +5,18 @@
import sys
import warnings
from functools import wraps
from typing import Optional

warnings.filterwarnings(action="ignore", message="Python 3.6 is no longer supported")

# pylint: disable=wrong-import-position

import click
import cloup

from ch_tools import __version__
from ch_tools.common.cli.context_settings import CONTEXT_SETTINGS
from ch_tools.common.cli.locale_resolver import LocaleResolver
from ch_tools.common.result import Status
from ch_tools.monrun_checks.ch_backup import backup_command
from ch_tools.monrun_checks.ch_core_dumps import core_dumps_command
@@ -33,13 +38,30 @@
LOG_FILE = "/var/log/clickhouse-monitoring/clickhouse-monitoring.log"
DEFAULT_USER = "monitor"

# pylint: disable=too-many-ancestors


class MonrunChecks(cloup.Group):
def add_command(
self,
cmd: click.Command,
name: Optional[str] = None,
section: Optional[cloup.Section] = None,
fallback_to_default_section: bool = True,
) -> None:
if cmd.callback is None:
super().add_command(
cmd,
name=name,
section=section,
fallback_to_default_section=fallback_to_default_section,
)
return

class MonrunChecks(click.Group):
def add_command(self, cmd, name=None):
cmd_callback = cmd.callback

@wraps(cmd_callback)
@click.pass_context
@cloup.pass_context
def callback_wrapper(ctx, *args, **kwargs):
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
@@ -75,23 +97,26 @@ def callback_wrapper(ctx, *args, **kwargs):
status.report()

cmd.callback = callback_wrapper
super().add_command(cmd, name=name)
super().add_command(
cmd,
name=name,
section=section,
fallback_to_default_section=fallback_to_default_section,
)


@click.group(
@cloup.group(
cls=MonrunChecks,
context_settings={
"help_option_names": ["-h", "--help"],
"terminal_width": 120,
},
context_settings=CONTEXT_SETTINGS,
)
@click.option(
@cloup.option(
"--no-user-check",
"no_user_check",
is_flag=True,
default=False,
help="Do not check current user.",
)
@cloup.version_option(__version__)
def cli(no_user_check):
if not no_user_check:
check_current_user()
@@ -124,8 +149,8 @@ def main():
"""
Program entry point.
"""
# pylint: disable=no-value-for-parameter
cli()
LocaleResolver.resolve()
cli.main()


def check_current_user():
@@ -148,3 +173,7 @@ def check_current_user():
except Exception as exc:
print(repr(exc), file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
60 changes: 48 additions & 12 deletions src/ch_tools/monrun_checks_keeper/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import logging
import os
from functools import wraps
from typing import Optional

import click
from click import group, option, pass_context
import cloup

from ch_tools import __version__
from ch_tools.common.cli.context_settings import CONTEXT_SETTINGS
from ch_tools.common.cli.locale_resolver import LocaleResolver
from ch_tools.common.result import Status
from ch_tools.monrun_checks_keeper.keeper_commands import (
alive_command,
@@ -21,13 +25,30 @@

LOG_FILE = "/var/log/keeper-monitoring/keeper-monitoring.log"

# pylint: disable=too-many-ancestors


class KeeperChecks(cloup.Group):
def add_command(
self,
cmd: click.Command,
name: Optional[str] = None,
section: Optional[cloup.Section] = None,
fallback_to_default_section: bool = True,
) -> None:
if cmd.callback is None:
super().add_command(
cmd,
name=name,
section=section,
fallback_to_default_section=fallback_to_default_section,
)
return

class KeeperChecks(click.Group):
def add_command(self, cmd, name=None):
cmd_callback = cmd.callback

@wraps(cmd_callback)
@click.pass_context
@cloup.pass_context
def wrapper(ctx, *a, **kw):
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
@@ -58,28 +79,39 @@ def wrapper(ctx, *a, **kw):
status.report()

cmd.callback = wrapper
super().add_command(cmd, name=name)
super().add_command(
cmd,
name=name,
section=section,
fallback_to_default_section=fallback_to_default_section,
)


@group(cls=KeeperChecks, context_settings={"help_option_names": ["-h", "--help"]})
@option("-r", "--retries", "retries", type=int, default=3, help="Number of retries")
@option(
@cloup.group(
cls=KeeperChecks,
context_settings=CONTEXT_SETTINGS,
)
@cloup.option(
"-r", "--retries", "retries", type=int, default=3, help="Number of retries"
)
@cloup.option(
"-t",
"--timeout",
"timeout",
type=float,
default=0.5,
help="Connection timeout (in seconds)",
)
@option(
@cloup.option(
"-n",
"--no-verify-ssl-certs",
"no_verify_ssl_certs",
is_flag=True,
default=False,
help="Allow unverified SSL certificates, e.g. self-signed ones",
)
@pass_context
@cloup.version_option(__version__)
@cloup.pass_context
def cli(ctx, retries, timeout, no_verify_ssl_certs):
ctx.obj = dict(
retries=retries, timeout=timeout, no_verify_ssl_certs=no_verify_ssl_certs
@@ -108,5 +140,9 @@ def main():
"""
Program entry point.
"""
# pylint: disable=no-value-for-parameter
cli()
LocaleResolver.resolve()
cli.main()


if __name__ == "__main__":
main()

0 comments on commit 20c47d8

Please sign in to comment.