nginx-lens 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nginx_lens-0.3.0/nginx_lens.egg-info → nginx_lens-0.3.2}/PKG-INFO +2 -1
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/health.py +10 -4
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/resolve.py +9 -2
- {nginx_lens-0.3.0 → nginx_lens-0.3.2/nginx_lens.egg-info}/PKG-INFO +2 -1
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/nginx_lens.egg-info/requires.txt +1 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/setup.py +2 -1
- nginx_lens-0.3.2/upstream_checker/checker.py +255 -0
- nginx_lens-0.3.0/upstream_checker/checker.py +0 -151
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/LICENSE +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/README.md +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/__init__.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/base.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/conflicts.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/dead_locations.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/diff.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/duplicates.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/empty_blocks.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/include.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/rewrite.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/route.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/unused.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/analyzer/warnings.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/__init__.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/analyze.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/cli.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/diff.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/graph.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/include.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/logs.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/route.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/syntax.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/commands/tree.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/exporter/__init__.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/exporter/graph.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/exporter/html.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/exporter/markdown.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/nginx_lens.egg-info/SOURCES.txt +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/nginx_lens.egg-info/dependency_links.txt +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/nginx_lens.egg-info/entry_points.txt +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/nginx_lens.egg-info/top_level.txt +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/parser/__init__.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/parser/nginx_parser.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/pyproject.toml +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/setup.cfg +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/tests/test_conflicts.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/tests/test_duplicates.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/tests/test_empty_blocks.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/tests/test_health.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/tests/test_parser.py +0 -0
- {nginx_lens-0.3.0 → nginx_lens-0.3.2}/upstream_checker/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nginx-lens
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
|
|
5
5
|
Author: Daniil Astrouski
|
|
6
6
|
Author-email: shelovesuastra@gmail.com
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Requires-Dist: typer[all]>=0.9.0
|
|
10
10
|
Requires-Dist: rich>=13.0.0
|
|
11
11
|
Requires-Dist: requests>=2.25.0
|
|
12
|
+
Requires-Dist: dnspython>=2.0.0
|
|
12
13
|
Dynamic: author
|
|
13
14
|
Dynamic: author-email
|
|
14
15
|
Dynamic: license-file
|
|
@@ -51,15 +51,21 @@ def health(
|
|
|
51
51
|
color = "green" if srv["healthy"] else "red"
|
|
52
52
|
|
|
53
53
|
if resolve:
|
|
54
|
-
|
|
54
|
+
resolved_list = []
|
|
55
55
|
if name in resolved_info:
|
|
56
56
|
for resolved_srv in resolved_info[name]:
|
|
57
57
|
if resolved_srv["address"] == srv["address"]:
|
|
58
|
-
|
|
58
|
+
resolved_list = resolved_srv["resolved"]
|
|
59
59
|
break
|
|
60
60
|
|
|
61
|
-
if
|
|
62
|
-
|
|
61
|
+
if resolved_list:
|
|
62
|
+
# Показываем все IP-адреса через запятую
|
|
63
|
+
resolved_str = ", ".join(resolved_list)
|
|
64
|
+
# Если есть "invalid resolve", показываем красным, иначе зеленым
|
|
65
|
+
if any("invalid resolve" in r for r in resolved_list):
|
|
66
|
+
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[red]{resolved_str}[/red]")
|
|
67
|
+
else:
|
|
68
|
+
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
|
|
63
69
|
else:
|
|
64
70
|
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", "[yellow]Failed to resolve[/yellow]")
|
|
65
71
|
else:
|
|
@@ -40,8 +40,15 @@ def resolve(
|
|
|
40
40
|
for name, servers in results.items():
|
|
41
41
|
for idx, srv in enumerate(servers):
|
|
42
42
|
upstream_name = name if idx == 0 else ""
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
resolved_list = srv["resolved"]
|
|
44
|
+
if resolved_list:
|
|
45
|
+
# Показываем все IP-адреса через запятую
|
|
46
|
+
resolved_str = ", ".join(resolved_list)
|
|
47
|
+
# Если есть "invalid resolve", показываем красным, иначе зеленым
|
|
48
|
+
if any("invalid resolve" in r for r in resolved_list):
|
|
49
|
+
table.add_row(upstream_name, srv["address"], f"[red]{resolved_str}[/red]")
|
|
50
|
+
else:
|
|
51
|
+
table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
|
|
45
52
|
else:
|
|
46
53
|
table.add_row(upstream_name, srv["address"], "[red]Failed to resolve[/red]")
|
|
47
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nginx-lens
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
|
|
5
5
|
Author: Daniil Astrouski
|
|
6
6
|
Author-email: shelovesuastra@gmail.com
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Requires-Dist: typer[all]>=0.9.0
|
|
10
10
|
Requires-Dist: rich>=13.0.0
|
|
11
11
|
Requires-Dist: requests>=2.25.0
|
|
12
|
+
Requires-Dist: dnspython>=2.0.0
|
|
12
13
|
Dynamic: author
|
|
13
14
|
Dynamic: author-email
|
|
14
15
|
Dynamic: license-file
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="nginx-lens",
|
|
5
|
-
version="0.3.
|
|
5
|
+
version="0.3.2",
|
|
6
6
|
description="CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx",
|
|
7
7
|
author="Daniil Astrouski",
|
|
8
8
|
author_email="shelovesuastra@gmail.com",
|
|
@@ -12,6 +12,7 @@ setup(
|
|
|
12
12
|
"typer[all]>=0.9.0",
|
|
13
13
|
"rich>=13.0.0",
|
|
14
14
|
"requests>=2.25.0",
|
|
15
|
+
"dnspython>=2.0.0",
|
|
15
16
|
],
|
|
16
17
|
entry_points={
|
|
17
18
|
"console_scripts": [
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# upstream_checker/checker.py
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import time
|
|
5
|
+
import http.client
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
try:
|
|
8
|
+
import dns.resolver
|
|
9
|
+
import dns.exception
|
|
10
|
+
DNS_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
DNS_AVAILABLE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_tcp(address: str, timeout: float, retries: int) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Проверка доступности сервера по TCP.
|
|
18
|
+
Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
|
|
19
|
+
"""
|
|
20
|
+
# Берем только host:port, игнорируем параметры
|
|
21
|
+
host_port = address.split()[0]
|
|
22
|
+
host, port = host_port.split(":")
|
|
23
|
+
port = int(port)
|
|
24
|
+
|
|
25
|
+
for _ in range(retries):
|
|
26
|
+
try:
|
|
27
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
28
|
+
return True
|
|
29
|
+
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
30
|
+
time.sleep(0.2)
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_http(address: str, timeout: float, retries: int) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Проверка доступности сервера по HTTP (GET /).
|
|
37
|
+
Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
|
|
38
|
+
"""
|
|
39
|
+
host_port = address.split()[0]
|
|
40
|
+
host, port = host_port.split(":")
|
|
41
|
+
port = int(port)
|
|
42
|
+
|
|
43
|
+
for _ in range(retries):
|
|
44
|
+
try:
|
|
45
|
+
conn = http.client.HTTPConnection(host, port, timeout=timeout)
|
|
46
|
+
conn.request("GET", "/")
|
|
47
|
+
resp = conn.getresponse()
|
|
48
|
+
healthy = resp.status < 500
|
|
49
|
+
conn.close()
|
|
50
|
+
if healthy:
|
|
51
|
+
return True
|
|
52
|
+
except Exception:
|
|
53
|
+
time.sleep(0.2)
|
|
54
|
+
continue
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_address(address: str) -> List[str]:
|
|
59
|
+
"""
|
|
60
|
+
Резолвит адрес upstream сервера в IP-адреса с информацией о CNAME.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
address: Адрес в формате "host:port" или "host:port параметры"
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Список строк в формате:
|
|
67
|
+
- "ip:port" - если резолвинг успешен без CNAME
|
|
68
|
+
- "ip:port (via cname.example.com)" - если есть CNAME и все ок
|
|
69
|
+
- "invalid resolve (via cname.example.com -> TXT)" - если CNAME ведет на невалидную запись
|
|
70
|
+
Пустой список, если резолвинг не удался
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
host_port = address.split()[0]
|
|
74
|
+
|
|
75
|
+
if ":" not in host_port:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
parts = host_port.rsplit(":", 1)
|
|
79
|
+
if len(parts) != 2:
|
|
80
|
+
return []
|
|
81
|
+
host, port = parts
|
|
82
|
+
|
|
83
|
+
# Если это уже IPv4 адрес, возвращаем как есть
|
|
84
|
+
try:
|
|
85
|
+
socket.inet_aton(host)
|
|
86
|
+
return [host_port]
|
|
87
|
+
except socket.error:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# Проверяем IPv6 (в квадратных скобках)
|
|
91
|
+
if host.startswith("[") and host.endswith("]"):
|
|
92
|
+
ipv6_host = host[1:-1]
|
|
93
|
+
try:
|
|
94
|
+
socket.inet_pton(socket.AF_INET6, ipv6_host)
|
|
95
|
+
return [host_port]
|
|
96
|
+
except (socket.error, OSError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Используем dnspython для детального DNS резолвинга
|
|
100
|
+
if DNS_AVAILABLE:
|
|
101
|
+
return _resolve_with_dns(host, port)
|
|
102
|
+
else:
|
|
103
|
+
# Fallback на стандартный socket, если dnspython недоступен
|
|
104
|
+
return _resolve_with_socket(host, port)
|
|
105
|
+
except (ValueError, IndexError, AttributeError):
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _resolve_with_dns(host: str, port: str) -> List[str]:
|
|
110
|
+
"""Резолвит DNS с использованием dnspython для получения информации о CNAME."""
|
|
111
|
+
try:
|
|
112
|
+
cname_info = None
|
|
113
|
+
invalid_type = None
|
|
114
|
+
|
|
115
|
+
# Сначала проверяем CNAME для исходного хоста
|
|
116
|
+
try:
|
|
117
|
+
cname_answer = dns.resolver.resolve(host, 'CNAME', raise_on_no_answer=False)
|
|
118
|
+
if cname_answer:
|
|
119
|
+
cname_target = str(cname_answer[0].target).rstrip('.')
|
|
120
|
+
cname_info = cname_target
|
|
121
|
+
|
|
122
|
+
# Если есть CNAME, проверяем A записи для CNAME цели (не для исходного хоста)
|
|
123
|
+
try:
|
|
124
|
+
a_answer = dns.resolver.resolve(cname_target, 'A', raise_on_no_answer=False)
|
|
125
|
+
if a_answer:
|
|
126
|
+
# CNAME ведет на валидные A записи
|
|
127
|
+
resolved_ips = []
|
|
128
|
+
for rdata in a_answer:
|
|
129
|
+
ip = str(rdata.address)
|
|
130
|
+
resolved_ips.append(f"{ip}:{port} (via {cname_info})")
|
|
131
|
+
return resolved_ips
|
|
132
|
+
else:
|
|
133
|
+
# Нет A записей для CNAME цели, проверяем другие типы
|
|
134
|
+
try:
|
|
135
|
+
txt_answer = dns.resolver.resolve(cname_target, 'TXT', raise_on_no_answer=False)
|
|
136
|
+
if txt_answer:
|
|
137
|
+
invalid_type = 'TXT'
|
|
138
|
+
except:
|
|
139
|
+
pass
|
|
140
|
+
if not invalid_type:
|
|
141
|
+
try:
|
|
142
|
+
mx_answer = dns.resolver.resolve(cname_target, 'MX', raise_on_no_answer=False)
|
|
143
|
+
if mx_answer:
|
|
144
|
+
invalid_type = 'MX'
|
|
145
|
+
except:
|
|
146
|
+
pass
|
|
147
|
+
if not invalid_type:
|
|
148
|
+
# Проверяем другие невалидные типы
|
|
149
|
+
try:
|
|
150
|
+
ns_answer = dns.resolver.resolve(cname_target, 'NS', raise_on_no_answer=False)
|
|
151
|
+
if ns_answer:
|
|
152
|
+
invalid_type = 'NS'
|
|
153
|
+
except:
|
|
154
|
+
pass
|
|
155
|
+
if invalid_type:
|
|
156
|
+
return [f"invalid resolve (via {cname_info} -> {invalid_type})"]
|
|
157
|
+
else:
|
|
158
|
+
return [f"invalid resolve (via {cname_info})"]
|
|
159
|
+
except Exception:
|
|
160
|
+
return [f"invalid resolve (via {cname_info})"]
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Если нет CNAME, получаем A записи для исходного хоста
|
|
165
|
+
try:
|
|
166
|
+
a_answer = dns.resolver.resolve(host, 'A', raise_on_no_answer=False)
|
|
167
|
+
if a_answer:
|
|
168
|
+
resolved_ips = []
|
|
169
|
+
for rdata in a_answer:
|
|
170
|
+
ip = str(rdata.address)
|
|
171
|
+
resolved_ips.append(f"{ip}:{port}")
|
|
172
|
+
return resolved_ips if resolved_ips else []
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
return []
|
|
177
|
+
except Exception:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _resolve_with_socket(host: str, port: str) -> List[str]:
|
|
182
|
+
"""Fallback резолвинг через socket (без информации о CNAME)."""
|
|
183
|
+
try:
|
|
184
|
+
# gethostbyname_ex возвращает (hostname, aliaslist, ipaddrlist)
|
|
185
|
+
_, _, ipaddrlist = socket.gethostbyname_ex(host)
|
|
186
|
+
# Фильтруем только IPv4 адреса (IPv6 обрабатываются отдельно)
|
|
187
|
+
resolved_ips = []
|
|
188
|
+
for ip in ipaddrlist:
|
|
189
|
+
try:
|
|
190
|
+
# Проверяем, что это IPv4
|
|
191
|
+
socket.inet_aton(ip)
|
|
192
|
+
resolved_ips.append(f"{ip}:{port}")
|
|
193
|
+
except socket.error:
|
|
194
|
+
pass
|
|
195
|
+
return resolved_ips if resolved_ips else []
|
|
196
|
+
except (socket.gaierror, OSError):
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def resolve_upstreams(
|
|
201
|
+
upstreams: Dict[str, List[str]]
|
|
202
|
+
) -> Dict[str, List[dict]]:
|
|
203
|
+
"""
|
|
204
|
+
Резолвит DNS имена upstream-серверов в IP-адреса.
|
|
205
|
+
|
|
206
|
+
Возвращает:
|
|
207
|
+
{
|
|
208
|
+
"backend": [
|
|
209
|
+
{"address": "example.com:8080", "resolved": ["192.168.1.1:8080", "192.168.1.2:8080"]},
|
|
210
|
+
{"address": "127.0.0.1:8080", "resolved": ["127.0.0.1:8080"]},
|
|
211
|
+
{"address": "badhost:80", "resolved": []},
|
|
212
|
+
...
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
"""
|
|
216
|
+
results = {}
|
|
217
|
+
for name, servers in upstreams.items():
|
|
218
|
+
results[name] = []
|
|
219
|
+
for srv in servers:
|
|
220
|
+
resolved = resolve_address(srv)
|
|
221
|
+
results[name].append({
|
|
222
|
+
"address": srv,
|
|
223
|
+
"resolved": resolved
|
|
224
|
+
})
|
|
225
|
+
return results
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def check_upstreams(
|
|
229
|
+
upstreams: Dict[str, List[str]],
|
|
230
|
+
timeout: float = 2.0,
|
|
231
|
+
retries: int = 1,
|
|
232
|
+
mode: str = "tcp"
|
|
233
|
+
) -> Dict[str, List[dict]]:
|
|
234
|
+
"""
|
|
235
|
+
Проверяет доступность upstream-серверов.
|
|
236
|
+
mode: "tcp" (по умолчанию) или "http"
|
|
237
|
+
|
|
238
|
+
Возвращает:
|
|
239
|
+
{
|
|
240
|
+
"backend": [
|
|
241
|
+
{"address": "127.0.0.1:8080", "healthy": True},
|
|
242
|
+
...
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
"""
|
|
246
|
+
results = {}
|
|
247
|
+
for name, servers in upstreams.items():
|
|
248
|
+
results[name] = []
|
|
249
|
+
for srv in servers:
|
|
250
|
+
if mode.lower() == "http":
|
|
251
|
+
healthy = check_http(srv, timeout, retries)
|
|
252
|
+
else:
|
|
253
|
+
healthy = check_tcp(srv, timeout, retries)
|
|
254
|
+
results[name].append({"address": srv, "healthy": healthy})
|
|
255
|
+
return results
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# upstream_checker/checker.py
|
|
2
|
-
|
|
3
|
-
import socket
|
|
4
|
-
import time
|
|
5
|
-
import http.client
|
|
6
|
-
from typing import Dict, List, Optional
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def check_tcp(address: str, timeout: float, retries: int) -> bool:
|
|
10
|
-
"""
|
|
11
|
-
Проверка доступности сервера по TCP.
|
|
12
|
-
Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
|
|
13
|
-
"""
|
|
14
|
-
# Берем только host:port, игнорируем параметры
|
|
15
|
-
host_port = address.split()[0]
|
|
16
|
-
host, port = host_port.split(":")
|
|
17
|
-
port = int(port)
|
|
18
|
-
|
|
19
|
-
for _ in range(retries):
|
|
20
|
-
try:
|
|
21
|
-
with socket.create_connection((host, port), timeout=timeout):
|
|
22
|
-
return True
|
|
23
|
-
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
24
|
-
time.sleep(0.2)
|
|
25
|
-
return False
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def check_http(address: str, timeout: float, retries: int) -> bool:
|
|
29
|
-
"""
|
|
30
|
-
Проверка доступности сервера по HTTP (GET /).
|
|
31
|
-
Ignores extra upstream options like 'max_fails' or 'fail_timeout'.
|
|
32
|
-
"""
|
|
33
|
-
host_port = address.split()[0]
|
|
34
|
-
host, port = host_port.split(":")
|
|
35
|
-
port = int(port)
|
|
36
|
-
|
|
37
|
-
for _ in range(retries):
|
|
38
|
-
try:
|
|
39
|
-
conn = http.client.HTTPConnection(host, port, timeout=timeout)
|
|
40
|
-
conn.request("GET", "/")
|
|
41
|
-
resp = conn.getresponse()
|
|
42
|
-
healthy = resp.status < 500
|
|
43
|
-
conn.close()
|
|
44
|
-
if healthy:
|
|
45
|
-
return True
|
|
46
|
-
except Exception:
|
|
47
|
-
time.sleep(0.2)
|
|
48
|
-
continue
|
|
49
|
-
return False
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def resolve_address(address: str) -> Optional[str]:
|
|
53
|
-
"""
|
|
54
|
-
Резолвит адрес upstream сервера в IP-адрес.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
address: Адрес в формате "host:port" или "host:port параметры"
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
IP-адрес в формате "ip:port" или None, если резолвинг не удался
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
host_port = address.split()[0]
|
|
64
|
-
|
|
65
|
-
if ":" not in host_port:
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
parts = host_port.rsplit(":", 1)
|
|
69
|
-
if len(parts) != 2:
|
|
70
|
-
return None
|
|
71
|
-
host, port = parts
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
socket.inet_aton(host)
|
|
75
|
-
return host_port
|
|
76
|
-
except socket.error:
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
if host.startswith("[") and host.endswith("]"):
|
|
80
|
-
ipv6_host = host[1:-1]
|
|
81
|
-
try:
|
|
82
|
-
socket.inet_pton(socket.AF_INET6, ipv6_host)
|
|
83
|
-
return host_port
|
|
84
|
-
except (socket.error, OSError):
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
ip = socket.gethostbyname(host)
|
|
89
|
-
return f"{ip}:{port}"
|
|
90
|
-
except (socket.gaierror, OSError):
|
|
91
|
-
return None
|
|
92
|
-
except (ValueError, IndexError, AttributeError):
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def resolve_upstreams(
|
|
97
|
-
upstreams: Dict[str, List[str]]
|
|
98
|
-
) -> Dict[str, List[dict]]:
|
|
99
|
-
"""
|
|
100
|
-
Резолвит DNS имена upstream-серверов в IP-адреса.
|
|
101
|
-
|
|
102
|
-
Возвращает:
|
|
103
|
-
{
|
|
104
|
-
"backend": [
|
|
105
|
-
{"address": "example.com:8080", "resolved": "192.168.1.1:8080"},
|
|
106
|
-
{"address": "127.0.0.1:8080", "resolved": "127.0.0.1:8080"},
|
|
107
|
-
{"address": "badhost:80", "resolved": None},
|
|
108
|
-
...
|
|
109
|
-
]
|
|
110
|
-
}
|
|
111
|
-
"""
|
|
112
|
-
results = {}
|
|
113
|
-
for name, servers in upstreams.items():
|
|
114
|
-
results[name] = []
|
|
115
|
-
for srv in servers:
|
|
116
|
-
resolved = resolve_address(srv)
|
|
117
|
-
results[name].append({
|
|
118
|
-
"address": srv,
|
|
119
|
-
"resolved": resolved
|
|
120
|
-
})
|
|
121
|
-
return results
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def check_upstreams(
|
|
125
|
-
upstreams: Dict[str, List[str]],
|
|
126
|
-
timeout: float = 2.0,
|
|
127
|
-
retries: int = 1,
|
|
128
|
-
mode: str = "tcp"
|
|
129
|
-
) -> Dict[str, List[dict]]:
|
|
130
|
-
"""
|
|
131
|
-
Проверяет доступность upstream-серверов.
|
|
132
|
-
mode: "tcp" (по умолчанию) или "http"
|
|
133
|
-
|
|
134
|
-
Возвращает:
|
|
135
|
-
{
|
|
136
|
-
"backend": [
|
|
137
|
-
{"address": "127.0.0.1:8080", "healthy": True},
|
|
138
|
-
...
|
|
139
|
-
]
|
|
140
|
-
}
|
|
141
|
-
"""
|
|
142
|
-
results = {}
|
|
143
|
-
for name, servers in upstreams.items():
|
|
144
|
-
results[name] = []
|
|
145
|
-
for srv in servers:
|
|
146
|
-
if mode.lower() == "http":
|
|
147
|
-
healthy = check_http(srv, timeout, retries)
|
|
148
|
-
else:
|
|
149
|
-
healthy = check_tcp(srv, timeout, retries)
|
|
150
|
-
results[name].append({"address": srv, "healthy": healthy})
|
|
151
|
-
return results
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|