nginx-lens 0.3.1__tar.gz → 0.3.3__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.1/nginx_lens.egg-info → nginx_lens-0.3.3}/PKG-INFO +2 -1
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/health.py +17 -3
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/resolve.py +14 -4
- {nginx_lens-0.3.1 → nginx_lens-0.3.3/nginx_lens.egg-info}/PKG-INFO +2 -1
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/requires.txt +1 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/setup.py +2 -1
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/upstream_checker/checker.py +96 -18
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/LICENSE +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/README.md +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/__init__.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/base.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/conflicts.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/dead_locations.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/diff.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/duplicates.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/empty_blocks.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/include.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/rewrite.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/route.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/unused.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/warnings.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/__init__.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/analyze.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/cli.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/diff.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/graph.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/include.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/logs.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/route.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/syntax.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/tree.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/__init__.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/graph.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/html.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/markdown.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/SOURCES.txt +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/dependency_links.txt +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/entry_points.txt +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/top_level.txt +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/parser/__init__.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/parser/nginx_parser.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/pyproject.toml +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/setup.cfg +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_conflicts.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_duplicates.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_empty_blocks.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_health.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_parser.py +0 -0
- {nginx_lens-0.3.1 → nginx_lens-0.3.3}/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.3
|
|
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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import typer
|
|
2
3
|
from rich.console import Console
|
|
3
4
|
from rich.table import Table
|
|
@@ -22,14 +23,16 @@ def health(
|
|
|
22
23
|
nginx-lens health /etc/nginx/nginx.conf --timeout 5 --retries 3 --mode http
|
|
23
24
|
nginx-lens health /etc/nginx/nginx.conf --resolve
|
|
24
25
|
"""
|
|
26
|
+
exit_code = 0
|
|
27
|
+
|
|
25
28
|
try:
|
|
26
29
|
tree = parse_nginx_config(config_path)
|
|
27
30
|
except FileNotFoundError:
|
|
28
31
|
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
29
|
-
|
|
32
|
+
sys.exit(1)
|
|
30
33
|
except Exception as e:
|
|
31
34
|
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
32
|
-
|
|
35
|
+
sys.exit(1)
|
|
33
36
|
|
|
34
37
|
upstreams = tree.get_upstreams()
|
|
35
38
|
results = check_upstreams(upstreams, timeout=timeout, retries=retries, mode=mode.lower())
|
|
@@ -50,6 +53,10 @@ def health(
|
|
|
50
53
|
status = "Healthy" if srv["healthy"] else "Unhealthy"
|
|
51
54
|
color = "green" if srv["healthy"] else "red"
|
|
52
55
|
|
|
56
|
+
# Проверяем статус здоровья
|
|
57
|
+
if not srv["healthy"]:
|
|
58
|
+
exit_code = 1
|
|
59
|
+
|
|
53
60
|
if resolve:
|
|
54
61
|
resolved_list = []
|
|
55
62
|
if name in resolved_info:
|
|
@@ -61,10 +68,17 @@ def health(
|
|
|
61
68
|
if resolved_list:
|
|
62
69
|
# Показываем все IP-адреса через запятую
|
|
63
70
|
resolved_str = ", ".join(resolved_list)
|
|
64
|
-
|
|
71
|
+
# Если есть "invalid resolve", показываем красным, иначе зеленым
|
|
72
|
+
if any("invalid resolve" in r for r in resolved_list):
|
|
73
|
+
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[red]{resolved_str}[/red]")
|
|
74
|
+
exit_code = 1
|
|
75
|
+
else:
|
|
76
|
+
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
|
|
65
77
|
else:
|
|
66
78
|
table.add_row(srv["address"], f"[{color}]{status}[/{color}]", "[yellow]Failed to resolve[/yellow]")
|
|
79
|
+
exit_code = 1
|
|
67
80
|
else:
|
|
68
81
|
table.add_row(srv["address"], f"[{color}]{status}[/{color}]")
|
|
69
82
|
|
|
70
83
|
console.print(table)
|
|
84
|
+
sys.exit(exit_code)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import typer
|
|
2
3
|
from rich.console import Console
|
|
3
4
|
from rich.table import Table
|
|
@@ -16,19 +17,21 @@ def resolve(
|
|
|
16
17
|
Пример:
|
|
17
18
|
nginx-lens resolve /etc/nginx/nginx.conf
|
|
18
19
|
"""
|
|
20
|
+
exit_code = 0
|
|
21
|
+
|
|
19
22
|
try:
|
|
20
23
|
tree = parse_nginx_config(config_path)
|
|
21
24
|
except FileNotFoundError:
|
|
22
25
|
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
23
|
-
|
|
26
|
+
sys.exit(1)
|
|
24
27
|
except Exception as e:
|
|
25
28
|
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
26
|
-
|
|
29
|
+
sys.exit(1)
|
|
27
30
|
|
|
28
31
|
upstreams = tree.get_upstreams()
|
|
29
32
|
if not upstreams:
|
|
30
33
|
console.print("[yellow]Не найдено ни одного upstream в конфигурации.[/yellow]")
|
|
31
|
-
|
|
34
|
+
sys.exit(0) # Нет upstream - это не ошибка, просто нет чего проверять
|
|
32
35
|
|
|
33
36
|
results = resolve_upstreams(upstreams)
|
|
34
37
|
|
|
@@ -44,9 +47,16 @@ def resolve(
|
|
|
44
47
|
if resolved_list:
|
|
45
48
|
# Показываем все IP-адреса через запятую
|
|
46
49
|
resolved_str = ", ".join(resolved_list)
|
|
47
|
-
|
|
50
|
+
# Если есть "invalid resolve", показываем красным, иначе зеленым
|
|
51
|
+
if any("invalid resolve" in r for r in resolved_list):
|
|
52
|
+
table.add_row(upstream_name, srv["address"], f"[red]{resolved_str}[/red]")
|
|
53
|
+
exit_code = 1
|
|
54
|
+
else:
|
|
55
|
+
table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
|
|
48
56
|
else:
|
|
49
57
|
table.add_row(upstream_name, srv["address"], "[red]Failed to resolve[/red]")
|
|
58
|
+
exit_code = 1
|
|
50
59
|
|
|
51
60
|
console.print(table)
|
|
61
|
+
sys.exit(exit_code)
|
|
52
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nginx-lens
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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.3",
|
|
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": [
|
|
@@ -4,6 +4,12 @@ import socket
|
|
|
4
4
|
import time
|
|
5
5
|
import http.client
|
|
6
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
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
def check_tcp(address: str, timeout: float, retries: int) -> bool:
|
|
@@ -51,13 +57,17 @@ def check_http(address: str, timeout: float, retries: int) -> bool:
|
|
|
51
57
|
|
|
52
58
|
def resolve_address(address: str) -> List[str]:
|
|
53
59
|
"""
|
|
54
|
-
Резолвит адрес upstream сервера в IP
|
|
60
|
+
Резолвит адрес upstream сервера в IP-адреса с информацией о CNAME.
|
|
55
61
|
|
|
56
62
|
Args:
|
|
57
63
|
address: Адрес в формате "host:port" или "host:port параметры"
|
|
58
64
|
|
|
59
65
|
Returns:
|
|
60
|
-
Список
|
|
66
|
+
Список строк в формате:
|
|
67
|
+
- "ip:port" - если резолвинг успешен без CNAME
|
|
68
|
+
- "ip:port (via cname.example.com)" - если есть CNAME и все ок
|
|
69
|
+
- "invalid resolve (via cname.example.com -> TXT)" - если CNAME ведет на невалидную запись
|
|
70
|
+
Пустой список, если резолвинг не удался
|
|
61
71
|
"""
|
|
62
72
|
try:
|
|
63
73
|
host_port = address.split()[0]
|
|
@@ -70,14 +80,12 @@ def resolve_address(address: str) -> List[str]:
|
|
|
70
80
|
return []
|
|
71
81
|
host, port = parts
|
|
72
82
|
|
|
73
|
-
# Если это уже IPv4 адрес, возвращаем как есть
|
|
74
83
|
try:
|
|
75
84
|
socket.inet_aton(host)
|
|
76
85
|
return [host_port]
|
|
77
86
|
except socket.error:
|
|
78
87
|
pass
|
|
79
88
|
|
|
80
|
-
# Проверяем IPv6 (в квадратных скобках)
|
|
81
89
|
if host.startswith("[") and host.endswith("]"):
|
|
82
90
|
ipv6_host = host[1:-1]
|
|
83
91
|
try:
|
|
@@ -86,23 +94,93 @@ def resolve_address(address: str) -> List[str]:
|
|
|
86
94
|
except (socket.error, OSError):
|
|
87
95
|
pass
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
if DNS_AVAILABLE:
|
|
98
|
+
return _resolve_with_dns(host, port)
|
|
99
|
+
else:
|
|
100
|
+
return _resolve_with_socket(host, port)
|
|
101
|
+
except (ValueError, IndexError, AttributeError):
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_with_dns(host: str, port: str) -> List[str]:
|
|
106
|
+
"""Резолвит DNS с использованием dnspython для получения информации о CNAME."""
|
|
107
|
+
try:
|
|
108
|
+
cname_info = None
|
|
109
|
+
invalid_type = None
|
|
110
|
+
|
|
90
111
|
try:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
cname_answer = dns.resolver.resolve(host, 'CNAME', raise_on_no_answer=False)
|
|
113
|
+
if cname_answer:
|
|
114
|
+
cname_target = str(cname_answer[0].target).rstrip('.')
|
|
115
|
+
cname_info = cname_target
|
|
116
|
+
|
|
96
117
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
a_answer = dns.resolver.resolve(cname_target, 'A', raise_on_no_answer=False)
|
|
119
|
+
if a_answer:
|
|
120
|
+
resolved_ips = []
|
|
121
|
+
for rdata in a_answer:
|
|
122
|
+
ip = str(rdata.address)
|
|
123
|
+
resolved_ips.append(f"{ip}:{port} (via {cname_info})")
|
|
124
|
+
return resolved_ips
|
|
125
|
+
else:
|
|
126
|
+
try:
|
|
127
|
+
txt_answer = dns.resolver.resolve(cname_target, 'TXT', raise_on_no_answer=False)
|
|
128
|
+
if txt_answer:
|
|
129
|
+
invalid_type = 'TXT'
|
|
130
|
+
except:
|
|
131
|
+
pass
|
|
132
|
+
if not invalid_type:
|
|
133
|
+
try:
|
|
134
|
+
mx_answer = dns.resolver.resolve(cname_target, 'MX', raise_on_no_answer=False)
|
|
135
|
+
if mx_answer:
|
|
136
|
+
invalid_type = 'MX'
|
|
137
|
+
except:
|
|
138
|
+
pass
|
|
139
|
+
if not invalid_type:
|
|
140
|
+
try:
|
|
141
|
+
ns_answer = dns.resolver.resolve(cname_target, 'NS', raise_on_no_answer=False)
|
|
142
|
+
if ns_answer:
|
|
143
|
+
invalid_type = 'NS'
|
|
144
|
+
except:
|
|
145
|
+
pass
|
|
146
|
+
if invalid_type:
|
|
147
|
+
return [f"invalid resolve (via {cname_info} -> {invalid_type})"]
|
|
148
|
+
else:
|
|
149
|
+
return [f"invalid resolve (via {cname_info})"]
|
|
150
|
+
except Exception:
|
|
151
|
+
return [f"invalid resolve (via {cname_info})"]
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
a_answer = dns.resolver.resolve(host, 'A', raise_on_no_answer=False)
|
|
157
|
+
if a_answer:
|
|
158
|
+
resolved_ips = []
|
|
159
|
+
for rdata in a_answer:
|
|
160
|
+
ip = str(rdata.address)
|
|
99
161
|
resolved_ips.append(f"{ip}:{port}")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
except
|
|
162
|
+
return resolved_ips if resolved_ips else []
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
return []
|
|
167
|
+
except Exception:
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _resolve_with_socket(host: str, port: str) -> List[str]:
|
|
172
|
+
"""Fallback резолвинг через socket (без информации о CNAME)."""
|
|
173
|
+
try:
|
|
174
|
+
_, _, ipaddrlist = socket.gethostbyname_ex(host)
|
|
175
|
+
resolved_ips = []
|
|
176
|
+
for ip in ipaddrlist:
|
|
177
|
+
try:
|
|
178
|
+
socket.inet_aton(ip)
|
|
179
|
+
resolved_ips.append(f"{ip}:{port}")
|
|
180
|
+
except socket.error:
|
|
181
|
+
pass
|
|
182
|
+
return resolved_ips if resolved_ips else []
|
|
183
|
+
except (socket.gaierror, OSError):
|
|
106
184
|
return []
|
|
107
185
|
|
|
108
186
|
|
|
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
|