nginx-lens 0.3.4__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- commands/analyze.py +9 -3
- commands/cli.py +6 -0
- commands/completion.py +174 -0
- commands/diff.py +3 -2
- commands/graph.py +3 -2
- commands/health.py +63 -6
- commands/include.py +3 -2
- commands/logs.py +267 -19
- commands/metrics.py +495 -0
- commands/resolve.py +58 -3
- commands/route.py +1 -0
- commands/syntax.py +1 -0
- commands/tree.py +3 -2
- commands/validate.py +451 -0
- config/__init__.py +4 -0
- config/config_loader.py +200 -0
- exporter/csv.py +87 -0
- exporter/json_yaml.py +361 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/METADATA +6 -1
- nginx_lens-0.5.0.dist-info/RECORD +48 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/top_level.txt +2 -0
- upstream_checker/checker.py +47 -7
- upstream_checker/dns_cache.py +216 -0
- utils/__init__.py +4 -0
- utils/progress.py +120 -0
- nginx_lens-0.3.4.dist-info/RECORD +0 -38
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/WHEEL +0 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/entry_points.txt +0 -0
- {nginx_lens-0.3.4.dist-info → nginx_lens-0.5.0.dist-info}/licenses/LICENSE +0 -0
commands/validate.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich import box
|
|
7
|
+
import subprocess
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, Tuple, List
|
|
10
|
+
|
|
11
|
+
from parser.nginx_parser import parse_nginx_config
|
|
12
|
+
from analyzer.conflicts import find_location_conflicts, find_listen_servername_conflicts
|
|
13
|
+
from analyzer.duplicates import find_duplicate_directives
|
|
14
|
+
from analyzer.empty_blocks import find_empty_blocks
|
|
15
|
+
from analyzer.warnings import find_warnings
|
|
16
|
+
from analyzer.unused import find_unused_variables
|
|
17
|
+
from analyzer.rewrite import find_rewrite_issues
|
|
18
|
+
from analyzer.dead_locations import find_dead_locations
|
|
19
|
+
from upstream_checker.checker import check_upstreams, resolve_upstreams
|
|
20
|
+
from upstream_checker.dns_cache import disable_cache, enable_cache
|
|
21
|
+
from config.config_loader import get_config
|
|
22
|
+
|
|
23
|
+
app = typer.Typer()
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
# Карта критичности для issue_type (из analyze.py)
|
|
27
|
+
ISSUE_META = {
|
|
28
|
+
'location_conflict': ("Возможное пересечение location. Это не всегда ошибка: порядок и типы location могут быть корректны. Проверьте, что порядок и типы location соответствуют вашим ожиданиям. Если всё ок — игнорируйте предупреждение.", "medium"),
|
|
29
|
+
'duplicate_directive': ("Оставьте только одну директиву с нужным значением в этом блоке.", "medium"),
|
|
30
|
+
'empty_block': ("Удалите или заполните пустой блок.", "low"),
|
|
31
|
+
'proxy_pass_no_scheme': ("Добавьте http:// или https:// в proxy_pass.", "medium"),
|
|
32
|
+
'autoindex_on': ("Отключите autoindex, если не требуется публикация файлов.", "medium"),
|
|
33
|
+
'if_block': ("Избегайте if внутри location, используйте map/try_files.", "medium"),
|
|
34
|
+
'server_tokens_on': ("Отключите server_tokens для безопасности.", "low"),
|
|
35
|
+
'ssl_missing': ("Укажите путь к SSL-сертификату/ключу.", "high"),
|
|
36
|
+
'ssl_protocols_weak': ("Отключите устаревшие протоколы TLS.", "high"),
|
|
37
|
+
'ssl_ciphers_weak': ("Используйте современные шифры.", "high"),
|
|
38
|
+
'listen_443_no_ssl': ("Добавьте ssl к listen 443.", "high"),
|
|
39
|
+
'listen_443_no_http2': ("Добавьте http2 к listen 443 для производительности.", "low"),
|
|
40
|
+
'no_limit_req_conn': ("Добавьте limit_req/limit_conn для защиты от DDoS.", "medium"),
|
|
41
|
+
'missing_security_header': ("Добавьте security-заголовок.", "medium"),
|
|
42
|
+
'deprecated': ("Замените устаревшую директиву.", "medium"),
|
|
43
|
+
'limit_too_small': ("Увеличьте лимит до рекомендуемого значения.", "medium"),
|
|
44
|
+
'limit_too_large': ("Уменьшите лимит до разумного значения.", "medium"),
|
|
45
|
+
'unused_variable': ("Удалите неиспользуемую переменную.", "low"),
|
|
46
|
+
'listen_servername_conflict': ("Измените listen/server_name для устранения конфликта.", "high"),
|
|
47
|
+
'rewrite_cycle': ("Проверьте rewrite на циклические правила.", "high"),
|
|
48
|
+
'rewrite_conflict': ("Проверьте порядок и уникальность rewrite.", "medium"),
|
|
49
|
+
'rewrite_no_flag': ("Добавьте last/break/redirect/permanent к rewrite.", "low"),
|
|
50
|
+
'dead_location': ("Удалите неиспользуемый location или используйте его.", "low"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
SEVERITY_COLOR = {"high": "red", "medium": "orange3", "low": "yellow"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate(
|
|
57
|
+
config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
|
|
58
|
+
nginx_path: str = typer.Option("nginx", "--nginx-path", help="Путь к бинарю nginx (по умолчанию 'nginx')"),
|
|
59
|
+
check_syntax: bool = typer.Option(True, "--syntax/--no-syntax", help="Проверять синтаксис через nginx -t"),
|
|
60
|
+
check_analysis: bool = typer.Option(True, "--analysis/--no-analysis", help="Выполнять анализ проблем"),
|
|
61
|
+
check_upstream: bool = typer.Option(True, "--upstream/--no-upstream", help="Проверять доступность upstream"),
|
|
62
|
+
check_dns: bool = typer.Option(False, "--dns/--no-dns", help="Проверять DNS резолвинг upstream"),
|
|
63
|
+
timeout: Optional[float] = typer.Option(None, "--timeout", help="Таймаут проверки upstream (сек)"),
|
|
64
|
+
max_workers: Optional[int] = typer.Option(None, "--max-workers", "-w", help="Максимальное количество потоков для параллельной обработки"),
|
|
65
|
+
json: bool = typer.Option(False, "--json", help="Экспортировать результаты в JSON"),
|
|
66
|
+
yaml: bool = typer.Option(False, "--yaml", help="Экспортировать результаты в YAML"),
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Комплексная валидация конфигурации Nginx.
|
|
70
|
+
|
|
71
|
+
Выполняет:
|
|
72
|
+
- Проверку синтаксиса (nginx -t)
|
|
73
|
+
- Анализ проблем и best practices
|
|
74
|
+
- Проверку доступности upstream серверов
|
|
75
|
+
- Опционально: DNS резолвинг upstream
|
|
76
|
+
|
|
77
|
+
Возвращает exit code 0 при успехе, 1 при наличии проблем.
|
|
78
|
+
Подходит для использования в CI/CD пайплайнах.
|
|
79
|
+
|
|
80
|
+
Пример:
|
|
81
|
+
nginx-lens validate /etc/nginx/nginx.conf
|
|
82
|
+
nginx-lens validate /etc/nginx/nginx.conf --no-upstream
|
|
83
|
+
nginx-lens validate /etc/nginx/nginx.conf --dns --json
|
|
84
|
+
"""
|
|
85
|
+
exit_code = 0
|
|
86
|
+
results = {
|
|
87
|
+
"syntax": {"valid": False, "errors": []},
|
|
88
|
+
"analysis": {"issues": [], "summary": {}},
|
|
89
|
+
"upstream": {"healthy": True, "servers": []},
|
|
90
|
+
"dns": {"resolved": True, "servers": []}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Загружаем конфигурацию
|
|
94
|
+
config = get_config()
|
|
95
|
+
defaults = config.get_defaults()
|
|
96
|
+
cache_config = config.get_cache_config()
|
|
97
|
+
validate_config = config.get_validate_config()
|
|
98
|
+
|
|
99
|
+
# Применяем значения из конфига, если не указаны через CLI
|
|
100
|
+
timeout_val = timeout if timeout is not None else defaults.get("timeout", 2.0)
|
|
101
|
+
max_workers_val = max_workers if max_workers is not None else defaults.get("max_workers", 10)
|
|
102
|
+
cache_ttl = cache_config.get("ttl", defaults.get("dns_cache_ttl", 300))
|
|
103
|
+
nginx_path_val = nginx_path if nginx_path is not None else validate_config.get("nginx_path", "nginx")
|
|
104
|
+
|
|
105
|
+
# Применяем настройки проверок из конфига, если не указаны через CLI
|
|
106
|
+
check_syntax_val = check_syntax if check_syntax is not None else validate_config.get("check_syntax", True)
|
|
107
|
+
check_analysis_val = check_analysis if check_analysis is not None else validate_config.get("check_analysis", True)
|
|
108
|
+
check_upstream_val = check_upstream if check_upstream is not None else validate_config.get("check_upstream", True)
|
|
109
|
+
check_dns_val = check_dns if check_dns is not None else validate_config.get("check_dns", False)
|
|
110
|
+
|
|
111
|
+
# Управление кэшем
|
|
112
|
+
enable_cache()
|
|
113
|
+
|
|
114
|
+
# 1. Проверка синтаксиса
|
|
115
|
+
if check_syntax_val:
|
|
116
|
+
console.print(Panel("[bold blue]1. Проверка синтаксиса[/bold blue]", box=box.ROUNDED))
|
|
117
|
+
syntax_valid, syntax_errors = _check_syntax(config_path, nginx_path_val)
|
|
118
|
+
results["syntax"]["valid"] = syntax_valid
|
|
119
|
+
results["syntax"]["errors"] = syntax_errors
|
|
120
|
+
|
|
121
|
+
if syntax_valid:
|
|
122
|
+
console.print("[green]✓ Синтаксис корректен[/green]")
|
|
123
|
+
else:
|
|
124
|
+
console.print("[red]✗ Обнаружены ошибки синтаксиса[/red]")
|
|
125
|
+
for error in syntax_errors:
|
|
126
|
+
console.print(f" [red]{error}[/red]")
|
|
127
|
+
exit_code = 1
|
|
128
|
+
|
|
129
|
+
# 2. Анализ проблем
|
|
130
|
+
tree = None
|
|
131
|
+
if check_analysis_val:
|
|
132
|
+
console.print(Panel("[bold blue]2. Анализ проблем и best practices[/bold blue]", box=box.ROUNDED))
|
|
133
|
+
try:
|
|
134
|
+
tree = parse_nginx_config(config_path)
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
conflicts = find_location_conflicts(tree)
|
|
143
|
+
dups = find_duplicate_directives(tree)
|
|
144
|
+
empties = find_empty_blocks(tree)
|
|
145
|
+
warnings = find_warnings(tree)
|
|
146
|
+
unused_vars = find_unused_variables(tree)
|
|
147
|
+
listen_conflicts = find_listen_servername_conflicts(tree)
|
|
148
|
+
rewrite_issues = find_rewrite_issues(tree)
|
|
149
|
+
dead_locations = find_dead_locations(tree)
|
|
150
|
+
|
|
151
|
+
# Собираем все проблемы
|
|
152
|
+
all_issues = []
|
|
153
|
+
high_severity_count = 0
|
|
154
|
+
|
|
155
|
+
for c in conflicts:
|
|
156
|
+
advice, severity = ISSUE_META.get('location_conflict', ("", "medium"))
|
|
157
|
+
all_issues.append({
|
|
158
|
+
"type": "location_conflict",
|
|
159
|
+
"severity": severity,
|
|
160
|
+
"message": f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}",
|
|
161
|
+
"advice": advice
|
|
162
|
+
})
|
|
163
|
+
if severity == "high":
|
|
164
|
+
high_severity_count += 1
|
|
165
|
+
|
|
166
|
+
for d in dups:
|
|
167
|
+
advice, severity = ISSUE_META.get('duplicate_directive', ("", "medium"))
|
|
168
|
+
loc = d.get('location')
|
|
169
|
+
all_issues.append({
|
|
170
|
+
"type": "duplicate_directive",
|
|
171
|
+
"severity": severity,
|
|
172
|
+
"message": f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}{' location: '+str(loc) if loc else ''}",
|
|
173
|
+
"advice": advice
|
|
174
|
+
})
|
|
175
|
+
if severity == "high":
|
|
176
|
+
high_severity_count += 1
|
|
177
|
+
|
|
178
|
+
for e in empties:
|
|
179
|
+
advice, severity = ISSUE_META.get('empty_block', ("", "low"))
|
|
180
|
+
all_issues.append({
|
|
181
|
+
"type": "empty_block",
|
|
182
|
+
"severity": severity,
|
|
183
|
+
"message": f"{e['block']} {e['arg'] or ''}",
|
|
184
|
+
"advice": advice
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
for w in warnings:
|
|
188
|
+
issue_type = w.get('type', '')
|
|
189
|
+
advice, severity = ISSUE_META.get(issue_type, ("", "medium"))
|
|
190
|
+
all_issues.append({
|
|
191
|
+
"type": issue_type,
|
|
192
|
+
"severity": severity,
|
|
193
|
+
"message": w.get('value', ''),
|
|
194
|
+
"advice": advice
|
|
195
|
+
})
|
|
196
|
+
if severity == "high":
|
|
197
|
+
high_severity_count += 1
|
|
198
|
+
|
|
199
|
+
for v in unused_vars:
|
|
200
|
+
advice, severity = ISSUE_META.get('unused_variable', ("", "low"))
|
|
201
|
+
all_issues.append({
|
|
202
|
+
"type": "unused_variable",
|
|
203
|
+
"severity": severity,
|
|
204
|
+
"message": v.get('name', ''),
|
|
205
|
+
"advice": advice
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
for c in listen_conflicts:
|
|
209
|
+
advice, severity = ISSUE_META.get('listen_servername_conflict', ("", "high"))
|
|
210
|
+
all_issues.append({
|
|
211
|
+
"type": "listen_servername_conflict",
|
|
212
|
+
"severity": severity,
|
|
213
|
+
"message": f"server1: {c.get('server1', {}).get('arg','')} server2: {c.get('server2', {}).get('arg','')}",
|
|
214
|
+
"advice": advice
|
|
215
|
+
})
|
|
216
|
+
high_severity_count += 1
|
|
217
|
+
|
|
218
|
+
for r in rewrite_issues:
|
|
219
|
+
advice, severity = ISSUE_META.get(r.get('type', ''), ("", "medium"))
|
|
220
|
+
all_issues.append({
|
|
221
|
+
"type": r.get('type', ''),
|
|
222
|
+
"severity": severity,
|
|
223
|
+
"message": r.get('value', ''),
|
|
224
|
+
"advice": advice
|
|
225
|
+
})
|
|
226
|
+
if severity == "high":
|
|
227
|
+
high_severity_count += 1
|
|
228
|
+
|
|
229
|
+
for l in dead_locations:
|
|
230
|
+
advice, severity = ISSUE_META.get('dead_location', ("", "low"))
|
|
231
|
+
all_issues.append({
|
|
232
|
+
"type": "dead_location",
|
|
233
|
+
"severity": severity,
|
|
234
|
+
"message": f"server: {l.get('server', {}).get('arg','')} location: {l.get('location', {}).get('arg','')}",
|
|
235
|
+
"advice": advice
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
results["analysis"]["issues"] = all_issues
|
|
239
|
+
results["analysis"]["summary"] = {
|
|
240
|
+
"total": len(all_issues),
|
|
241
|
+
"high": high_severity_count,
|
|
242
|
+
"medium": sum(1 for i in all_issues if i["severity"] == "medium"),
|
|
243
|
+
"low": sum(1 for i in all_issues if i["severity"] == "low")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if all_issues:
|
|
247
|
+
# Показываем только high и medium проблемы в таблице
|
|
248
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
249
|
+
table.add_column("Severity", style="bold")
|
|
250
|
+
table.add_column("Type")
|
|
251
|
+
table.add_column("Issue")
|
|
252
|
+
|
|
253
|
+
for issue in all_issues:
|
|
254
|
+
if issue["severity"] in ["high", "medium"]:
|
|
255
|
+
color = SEVERITY_COLOR.get(issue["severity"], "white")
|
|
256
|
+
table.add_row(
|
|
257
|
+
f"[{color}]{issue['severity'].upper()}[/{color}]",
|
|
258
|
+
issue["type"],
|
|
259
|
+
issue["message"]
|
|
260
|
+
)
|
|
261
|
+
if issue["severity"] == "high":
|
|
262
|
+
exit_code = 1
|
|
263
|
+
|
|
264
|
+
if table.row_count > 0:
|
|
265
|
+
console.print(table)
|
|
266
|
+
|
|
267
|
+
if high_severity_count > 0:
|
|
268
|
+
console.print(f"[red]✗ Найдено {high_severity_count} критических проблем[/red]")
|
|
269
|
+
exit_code = 1
|
|
270
|
+
elif len(all_issues) > 0:
|
|
271
|
+
console.print(f"[yellow]⚠ Найдено {len(all_issues)} проблем (все некритические)[/yellow]")
|
|
272
|
+
else:
|
|
273
|
+
console.print("[green]✓ Проблем не найдено[/green]")
|
|
274
|
+
|
|
275
|
+
# 3. Проверка upstream
|
|
276
|
+
if check_upstream_val:
|
|
277
|
+
console.print(Panel("[bold blue]3. Проверка доступности upstream[/bold blue]", box=box.ROUNDED))
|
|
278
|
+
if tree is None:
|
|
279
|
+
try:
|
|
280
|
+
tree = parse_nginx_config(config_path)
|
|
281
|
+
except FileNotFoundError:
|
|
282
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
upstreams = tree.get_upstreams()
|
|
289
|
+
if upstreams:
|
|
290
|
+
upstream_results = check_upstreams(upstreams, timeout=timeout_val, retries=1, mode="tcp", max_workers=max_workers_val)
|
|
291
|
+
|
|
292
|
+
unhealthy_count = 0
|
|
293
|
+
upstream_servers = []
|
|
294
|
+
|
|
295
|
+
for name, servers in upstream_results.items():
|
|
296
|
+
for srv in servers:
|
|
297
|
+
server_info = {
|
|
298
|
+
"upstream": name,
|
|
299
|
+
"address": srv["address"],
|
|
300
|
+
"healthy": srv["healthy"]
|
|
301
|
+
}
|
|
302
|
+
upstream_servers.append(server_info)
|
|
303
|
+
if not srv["healthy"]:
|
|
304
|
+
unhealthy_count += 1
|
|
305
|
+
|
|
306
|
+
results["upstream"]["servers"] = upstream_servers
|
|
307
|
+
results["upstream"]["healthy"] = unhealthy_count == 0
|
|
308
|
+
|
|
309
|
+
if unhealthy_count > 0:
|
|
310
|
+
console.print(f"[red]✗ Найдено {unhealthy_count} недоступных upstream серверов[/red]")
|
|
311
|
+
table = Table(show_header=True, header_style="bold red")
|
|
312
|
+
table.add_column("Upstream")
|
|
313
|
+
table.add_column("Address")
|
|
314
|
+
table.add_column("Status")
|
|
315
|
+
|
|
316
|
+
for srv in upstream_servers:
|
|
317
|
+
if not srv["healthy"]:
|
|
318
|
+
table.add_row(srv["upstream"], srv["address"], "[red]Unhealthy[/red]")
|
|
319
|
+
|
|
320
|
+
console.print(table)
|
|
321
|
+
exit_code = 1
|
|
322
|
+
else:
|
|
323
|
+
console.print(f"[green]✓ Все upstream серверы доступны ({len(upstream_servers)} серверов)[/green]")
|
|
324
|
+
else:
|
|
325
|
+
console.print("[yellow]⚠ Upstream серверы не найдены[/yellow]")
|
|
326
|
+
|
|
327
|
+
# 4. Проверка DNS резолвинга
|
|
328
|
+
if check_dns_val:
|
|
329
|
+
console.print(Panel("[bold blue]4. Проверка DNS резолвинга upstream[/bold blue]", box=box.ROUNDED))
|
|
330
|
+
if tree is None:
|
|
331
|
+
try:
|
|
332
|
+
tree = parse_nginx_config(config_path)
|
|
333
|
+
except FileNotFoundError:
|
|
334
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
|
|
340
|
+
upstreams = tree.get_upstreams()
|
|
341
|
+
if upstreams:
|
|
342
|
+
dns_results = resolve_upstreams(upstreams, max_workers=max_workers_val, use_cache=True, cache_ttl=cache_ttl)
|
|
343
|
+
|
|
344
|
+
failed_count = 0
|
|
345
|
+
invalid_count = 0
|
|
346
|
+
dns_servers = []
|
|
347
|
+
|
|
348
|
+
for name, servers in dns_results.items():
|
|
349
|
+
for srv in servers:
|
|
350
|
+
server_info = {
|
|
351
|
+
"upstream": name,
|
|
352
|
+
"address": srv["address"],
|
|
353
|
+
"resolved": srv["resolved"],
|
|
354
|
+
"status": "success"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if not srv["resolved"]:
|
|
358
|
+
server_info["status"] = "failed"
|
|
359
|
+
failed_count += 1
|
|
360
|
+
elif any("invalid resolve" in r for r in srv["resolved"]):
|
|
361
|
+
server_info["status"] = "invalid"
|
|
362
|
+
invalid_count += 1
|
|
363
|
+
|
|
364
|
+
dns_servers.append(server_info)
|
|
365
|
+
|
|
366
|
+
results["dns"]["servers"] = dns_servers
|
|
367
|
+
results["dns"]["resolved"] = (failed_count + invalid_count) == 0
|
|
368
|
+
|
|
369
|
+
if failed_count > 0 or invalid_count > 0:
|
|
370
|
+
console.print(f"[red]✗ Проблемы с DNS резолвингом: {failed_count} не резолвится, {invalid_count} невалидных[/red]")
|
|
371
|
+
table = Table(show_header=True, header_style="bold red")
|
|
372
|
+
table.add_column("Upstream")
|
|
373
|
+
table.add_column("Address")
|
|
374
|
+
table.add_column("Status")
|
|
375
|
+
|
|
376
|
+
for srv in dns_servers:
|
|
377
|
+
if srv["status"] != "success":
|
|
378
|
+
status_str = "Failed" if srv["status"] == "failed" else "Invalid"
|
|
379
|
+
resolved_str = ", ".join(srv["resolved"]) if srv["resolved"] else "Failed to resolve"
|
|
380
|
+
table.add_row(srv["upstream"], srv["address"], f"[red]{status_str}: {resolved_str}[/red]")
|
|
381
|
+
|
|
382
|
+
console.print(table)
|
|
383
|
+
exit_code = 1
|
|
384
|
+
else:
|
|
385
|
+
console.print(f"[green]✓ Все DNS имена резолвятся корректно ({len(dns_servers)} серверов)[/green]")
|
|
386
|
+
else:
|
|
387
|
+
console.print("[yellow]⚠ Upstream серверы не найдены[/yellow]")
|
|
388
|
+
|
|
389
|
+
# Экспорт результатов
|
|
390
|
+
if json or yaml:
|
|
391
|
+
from exporter.json_yaml import print_export
|
|
392
|
+
export_data = {
|
|
393
|
+
"timestamp": __import__('datetime').datetime.now().isoformat(),
|
|
394
|
+
"config_path": config_path,
|
|
395
|
+
"results": results,
|
|
396
|
+
"valid": exit_code == 0
|
|
397
|
+
}
|
|
398
|
+
format_type = 'json' if json else 'yaml'
|
|
399
|
+
print_export(export_data, format_type)
|
|
400
|
+
sys.exit(exit_code)
|
|
401
|
+
|
|
402
|
+
# Итоговый результат
|
|
403
|
+
console.print("")
|
|
404
|
+
if exit_code == 0:
|
|
405
|
+
console.print(Panel("[bold green]✓ Валидация пройдена успешно[/bold green]", box=box.ROUNDED))
|
|
406
|
+
else:
|
|
407
|
+
console.print(Panel("[bold red]✗ Валидация не пройдена. Обнаружены проблемы.[/bold red]", box=box.ROUNDED))
|
|
408
|
+
|
|
409
|
+
sys.exit(exit_code)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _check_syntax(config_path: str, nginx_path: str) -> Tuple[bool, List[str]]:
|
|
413
|
+
"""
|
|
414
|
+
Проверяет синтаксис конфигурации через nginx -t.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
config_path: Путь к конфигурационному файлу
|
|
418
|
+
nginx_path: Путь к бинарю nginx
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Кортеж (valid, errors) где valid - валидность, errors - список ошибок
|
|
422
|
+
"""
|
|
423
|
+
cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
|
|
424
|
+
|
|
425
|
+
# Если не root, пробуем через sudo
|
|
426
|
+
if hasattr(os, 'geteuid') and os.geteuid() != 0:
|
|
427
|
+
cmd = ["sudo"] + cmd
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=10)
|
|
431
|
+
|
|
432
|
+
if result.returncode == 0:
|
|
433
|
+
return True, []
|
|
434
|
+
else:
|
|
435
|
+
# Парсим ошибки из stderr
|
|
436
|
+
errors = []
|
|
437
|
+
error_output = result.stderr or result.stdout
|
|
438
|
+
if error_output:
|
|
439
|
+
# Простой парсинг ошибок nginx
|
|
440
|
+
for line in error_output.split('\n'):
|
|
441
|
+
if 'error' in line.lower() or 'failed' in line.lower():
|
|
442
|
+
errors.append(line.strip())
|
|
443
|
+
|
|
444
|
+
return False, errors if errors else [error_output.strip()] if error_output.strip() else ["Неизвестная ошибка синтаксиса"]
|
|
445
|
+
except FileNotFoundError:
|
|
446
|
+
return False, [f"Бинарь nginx не найден: {nginx_path}"]
|
|
447
|
+
except subprocess.TimeoutExpired:
|
|
448
|
+
return False, ["Таймаут при проверке синтаксиса"]
|
|
449
|
+
except Exception as e:
|
|
450
|
+
return False, [f"Ошибка при проверке синтаксиса: {e}"]
|
|
451
|
+
|
config/__init__.py
ADDED
config/config_loader.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль для загрузки и управления конфигурационным файлом nginx-lens.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import yaml
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigLoader:
|
|
11
|
+
"""
|
|
12
|
+
Загрузчик конфигурационного файла для nginx-lens.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Инициализация загрузчика конфигурации."""
|
|
17
|
+
self.config: Dict[str, Any] = {}
|
|
18
|
+
self.config_path: Optional[Path] = None
|
|
19
|
+
self._load_config()
|
|
20
|
+
|
|
21
|
+
def _find_config_file(self) -> Optional[Path]:
|
|
22
|
+
"""
|
|
23
|
+
Ищет конфигурационный файл в стандартных местах.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path к конфигурационному файлу или None
|
|
27
|
+
"""
|
|
28
|
+
# Список возможных путей к конфигу (в порядке приоритета)
|
|
29
|
+
possible_paths = [
|
|
30
|
+
Path.cwd() / ".nginx-lens.yaml", # Текущая директория
|
|
31
|
+
Path.cwd() / ".nginx-lens.yml",
|
|
32
|
+
Path("/opt/nginx-lens/config.yaml"), # Системный конфиг
|
|
33
|
+
Path("/opt/nginx-lens/config.yml"),
|
|
34
|
+
Path.home() / ".nginx-lens" / "config.yaml", # Домашняя директория
|
|
35
|
+
Path.home() / ".nginx-lens" / "config.yml",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for path in possible_paths:
|
|
39
|
+
if path.exists() and path.is_file():
|
|
40
|
+
return path
|
|
41
|
+
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def _load_config(self):
|
|
45
|
+
"""Загружает конфигурацию из файла."""
|
|
46
|
+
config_file = self._find_config_file()
|
|
47
|
+
|
|
48
|
+
if not config_file:
|
|
49
|
+
# Используем значения по умолчанию
|
|
50
|
+
self.config = self._get_default_config()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with open(config_file, 'r') as f:
|
|
55
|
+
self.config = yaml.safe_load(f) or {}
|
|
56
|
+
|
|
57
|
+
# Объединяем с дефолтными значениями
|
|
58
|
+
default_config = self._get_default_config()
|
|
59
|
+
self.config = self._merge_config(default_config, self.config)
|
|
60
|
+
self.config_path = config_file
|
|
61
|
+
except (yaml.YAMLError, IOError) as e:
|
|
62
|
+
# В случае ошибки используем дефолтные значения
|
|
63
|
+
self.config = self._get_default_config()
|
|
64
|
+
|
|
65
|
+
def _get_default_config(self) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Возвращает конфигурацию по умолчанию.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Словарь с дефолтными настройками
|
|
71
|
+
"""
|
|
72
|
+
return {
|
|
73
|
+
"defaults": {
|
|
74
|
+
"timeout": 2.0,
|
|
75
|
+
"retries": 1,
|
|
76
|
+
"mode": "tcp",
|
|
77
|
+
"max_workers": 10,
|
|
78
|
+
"dns_cache_ttl": 300,
|
|
79
|
+
"top": 10,
|
|
80
|
+
},
|
|
81
|
+
"output": {
|
|
82
|
+
"colors": True,
|
|
83
|
+
"format": "table", # table, json, yaml
|
|
84
|
+
},
|
|
85
|
+
"cache": {
|
|
86
|
+
"enabled": True,
|
|
87
|
+
"ttl": 300,
|
|
88
|
+
},
|
|
89
|
+
"validate": {
|
|
90
|
+
"check_syntax": True,
|
|
91
|
+
"check_analysis": True,
|
|
92
|
+
"check_upstream": True,
|
|
93
|
+
"check_dns": False,
|
|
94
|
+
"nginx_path": "nginx",
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def _merge_config(self, default: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Рекурсивно объединяет конфигурации.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
default: Конфигурация по умолчанию
|
|
104
|
+
user: Пользовательская конфигурация
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Объединенная конфигурация
|
|
108
|
+
"""
|
|
109
|
+
result = default.copy()
|
|
110
|
+
|
|
111
|
+
for key, value in user.items():
|
|
112
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
113
|
+
result[key] = self._merge_config(result[key], value)
|
|
114
|
+
else:
|
|
115
|
+
result[key] = value
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
def get(self, section: str, key: str, default: Any = None) -> Any:
|
|
120
|
+
"""
|
|
121
|
+
Получает значение из конфигурации.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
section: Секция конфига (например, "defaults", "output")
|
|
125
|
+
key: Ключ в секции
|
|
126
|
+
default: Значение по умолчанию, если не найдено
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Значение из конфига или default
|
|
130
|
+
"""
|
|
131
|
+
return self.config.get(section, {}).get(key, default)
|
|
132
|
+
|
|
133
|
+
def get_defaults(self) -> Dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Получает все значения по умолчанию.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Словарь с настройками по умолчанию
|
|
139
|
+
"""
|
|
140
|
+
return self.config.get("defaults", {})
|
|
141
|
+
|
|
142
|
+
def get_output_config(self) -> Dict[str, Any]:
|
|
143
|
+
"""
|
|
144
|
+
Получает настройки вывода.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Словарь с настройками вывода
|
|
148
|
+
"""
|
|
149
|
+
return self.config.get("output", {})
|
|
150
|
+
|
|
151
|
+
def get_cache_config(self) -> Dict[str, Any]:
|
|
152
|
+
"""
|
|
153
|
+
Получает настройки кэша.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Словарь с настройками кэша
|
|
157
|
+
"""
|
|
158
|
+
return self.config.get("cache", {})
|
|
159
|
+
|
|
160
|
+
def get_validate_config(self) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Получает настройки команды validate.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Словарь с настройками validate
|
|
166
|
+
"""
|
|
167
|
+
return self.config.get("validate", {})
|
|
168
|
+
|
|
169
|
+
def get_config_path(self) -> Optional[str]:
|
|
170
|
+
"""
|
|
171
|
+
Возвращает путь к загруженному конфигурационному файлу.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Путь к конфигу или None
|
|
175
|
+
"""
|
|
176
|
+
return str(self.config_path) if self.config_path else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Глобальный экземпляр загрузчика конфигурации
|
|
180
|
+
_config_loader: Optional[ConfigLoader] = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_config() -> ConfigLoader:
|
|
184
|
+
"""
|
|
185
|
+
Получает глобальный экземпляр загрузчика конфигурации.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Экземпляр ConfigLoader
|
|
189
|
+
"""
|
|
190
|
+
global _config_loader
|
|
191
|
+
if _config_loader is None:
|
|
192
|
+
_config_loader = ConfigLoader()
|
|
193
|
+
return _config_loader
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def reload_config():
|
|
197
|
+
"""Перезагружает конфигурацию."""
|
|
198
|
+
global _config_loader
|
|
199
|
+
_config_loader = ConfigLoader()
|
|
200
|
+
|