Skip to content
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

Try to resolve locale before running Click #32

Merged
merged 7 commits into from
Aug 3, 2023
Merged
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
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
Expand Up @@ -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:
Expand All @@ -157,8 +157,8 @@ mypy:

.PHONY: format
format:
isort .
black .
isort src tests
black src tests
myrrc marked this conversation as resolved.
Show resolved Hide resolved


.PHONY: help
Expand Down
8 changes: 8 additions & 0 deletions src/ch_tools/chadmin/chadmin_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -124,8 +149,8 @@ def main():
"""
Program entry point.
"""
# pylint: disable=no-value-for-parameter
cli()
LocaleResolver.resolve()
cli.main()


def check_current_user():
Expand All @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -108,5 +140,9 @@ def main():
"""
Program entry point.
"""
# pylint: disable=no-value-for-parameter
cli()
LocaleResolver.resolve()
cli.main()


if __name__ == "__main__":
main()