nginx-lens 0.1.5__tar.gz → 0.1.7__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.1.7/LICENSE +21 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/PKG-INFO +3 -1
- nginx_lens-0.1.7/README.md +81 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/analyze.py +8 -1
- nginx_lens-0.1.7/commands/diff.py +51 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/graph.py +60 -9
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/health.py +8 -1
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/include.py +8 -1
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/logs.py +25 -17
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/route.py +13 -6
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/syntax.py +7 -2
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/tree.py +8 -1
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/PKG-INFO +3 -1
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/SOURCES.txt +2 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/setup.py +1 -1
- nginx_lens-0.1.5/commands/diff.py +0 -39
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/__init__.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/base.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/conflicts.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/dead_locations.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/diff.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/duplicates.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/empty_blocks.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/include.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/rewrite.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/route.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/unused.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/analyzer/warnings.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/__init__.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/commands/cli.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/exporter/__init__.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/exporter/graph.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/exporter/html.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/exporter/markdown.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/dependency_links.txt +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/entry_points.txt +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/requires.txt +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/nginx_lens.egg-info/top_level.txt +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/parser/__init__.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/parser/nginx_parser.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/pyproject.toml +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/setup.cfg +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/tests/test_conflicts.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/tests/test_duplicates.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/tests/test_empty_blocks.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/tests/test_health.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/tests/test_parser.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/upstream_checker/__init__.py +0 -0
- {nginx_lens-0.1.5 → nginx_lens-0.1.7}/upstream_checker/checker.py +0 -0
nginx_lens-0.1.7/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniil Astrouski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nginx-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
|
|
5
5
|
Author: Daniil Astrouski
|
|
6
6
|
Author-email: shelovesuastra@gmail.com
|
|
7
7
|
Requires-Python: >=3.8
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Requires-Dist: typer[all]>=0.9.0
|
|
9
10
|
Requires-Dist: rich>=13.0.0
|
|
10
11
|
Requires-Dist: requests>=2.25.0
|
|
11
12
|
Dynamic: author
|
|
12
13
|
Dynamic: author-email
|
|
14
|
+
Dynamic: license-file
|
|
13
15
|
Dynamic: requires-dist
|
|
14
16
|
Dynamic: requires-python
|
|
15
17
|
Dynamic: summary
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# nginx-lens
|
|
2
|
+
|
|
3
|
+
**nginx-lens** — современный CLI-инструмент для анализа, диагностики и визуализации конфигураций Nginx.
|
|
4
|
+
|
|
5
|
+
## Зачем нужен nginx-lens?
|
|
6
|
+
|
|
7
|
+
- Быстро находит ошибки и потенциальные проблемы в ваших nginx-конфигах.
|
|
8
|
+
- Визуализирует маршруты и структуру — легко понять, как устроен ваш nginx.
|
|
9
|
+
- Показывает, какой location/server обслуживает конкретный URL.
|
|
10
|
+
- Анализирует логи и помогает выявить аномалии.
|
|
11
|
+
- Упрощает аудит, миграцию и поддержку сложных инфраструктур.
|
|
12
|
+
|
|
13
|
+
## Примеры работы
|
|
14
|
+
|
|
15
|
+
### Справочник утилиты
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
### Справочник для команд
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
### Доступность upstream-серверов
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
### Древовидная визуализация структуры конфига
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
### Древовидная визуализация include'ов
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
### Удобный анализатор логов
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
### Аудит конфигурации
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
### Визуализация маршрутов
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
### Поиск маршрута для URL
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
### Сравнение конфигов
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
### Удобная проверка синтаксиса конфига
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Установка и системные требования
|
|
61
|
+
|
|
62
|
+
- **Python 3.8+**
|
|
63
|
+
- Linux/macOS (работает и под Windows WSL)
|
|
64
|
+
|
|
65
|
+
### Установка через PyPI (рекомендуется)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pipx install nginx-lens
|
|
69
|
+
```
|
|
70
|
+
или
|
|
71
|
+
```bash
|
|
72
|
+
pip install nginx-lens
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Автор
|
|
76
|
+
|
|
77
|
+
[Daniil Astrouski](https://github.com/shelovesuastra)
|
|
78
|
+
|
|
79
|
+
## Лицензия
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -58,7 +58,14 @@ def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf"
|
|
|
58
58
|
Пример:
|
|
59
59
|
nginx-lens analyze /etc/nginx/nginx.conf
|
|
60
60
|
"""
|
|
61
|
-
|
|
61
|
+
try:
|
|
62
|
+
tree = parse_nginx_config(config_path)
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
65
|
+
return
|
|
66
|
+
except Exception as e:
|
|
67
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
68
|
+
return
|
|
62
69
|
conflicts = find_location_conflicts(tree)
|
|
63
70
|
dups = find_duplicate_directives(tree)
|
|
64
71
|
empties = find_empty_blocks(tree)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from analyzer.diff import diff_trees
|
|
5
|
+
from parser.nginx_parser import parse_nginx_config
|
|
6
|
+
import difflib
|
|
7
|
+
|
|
8
|
+
app = typer.Typer()
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
def diff(
|
|
12
|
+
config1: str = typer.Argument(..., help="Первый nginx.conf"),
|
|
13
|
+
config2: str = typer.Argument(..., help="Второй nginx.conf")
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Сравнивает две конфигурации Nginx и выводит отличия построчно side-by-side с подсветкой.
|
|
17
|
+
|
|
18
|
+
Пример:
|
|
19
|
+
nginx-lens diff /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
with open(config1) as f1, open(config2) as f2:
|
|
23
|
+
lines1 = f1.readlines()
|
|
24
|
+
lines2 = f2.readlines()
|
|
25
|
+
except FileNotFoundError as e:
|
|
26
|
+
console.print(f"[red]Файл {e.filename} не найден. Проверьте путь к конфигу.[/red]")
|
|
27
|
+
return
|
|
28
|
+
except Exception as e:
|
|
29
|
+
console.print(f"[red]Ошибка при чтении файлов: {e}[/red]")
|
|
30
|
+
return
|
|
31
|
+
maxlen = max(len(lines1), len(lines2))
|
|
32
|
+
# Выравниваем длины
|
|
33
|
+
lines1 += [''] * (maxlen - len(lines1))
|
|
34
|
+
lines2 += [''] * (maxlen - len(lines2))
|
|
35
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
36
|
+
table.add_column("№", style="dim", width=4)
|
|
37
|
+
table.add_column("Config 1", style="white")
|
|
38
|
+
table.add_column("№", style="dim", width=4)
|
|
39
|
+
table.add_column("Config 2", style="white")
|
|
40
|
+
for i in range(maxlen):
|
|
41
|
+
l1 = lines1[i].rstrip('\n')
|
|
42
|
+
l2 = lines2[i].rstrip('\n')
|
|
43
|
+
n1 = str(i+1) if l1 else ''
|
|
44
|
+
n2 = str(i+1) if l2 else ''
|
|
45
|
+
if l1 == l2:
|
|
46
|
+
table.add_row(n1, l1, n2, l2)
|
|
47
|
+
else:
|
|
48
|
+
style1 = "red" if l1 else "on red"
|
|
49
|
+
style2 = "green" if l2 else "on green"
|
|
50
|
+
table.add_row(f"[bold]{n1}[/bold]" if l1 else n1, f"[{style1}]{l1}[/{style1}]" if l1 or l2 else '', f"[bold]{n2}[/bold]" if l2 else n2, f"[{style2}]{l2}[/{style2}]" if l1 or l2 else '')
|
|
51
|
+
console.print(table)
|
|
@@ -16,7 +16,14 @@ def graph(
|
|
|
16
16
|
Пример:
|
|
17
17
|
nginx-lens graph /etc/nginx/nginx.conf
|
|
18
18
|
"""
|
|
19
|
-
|
|
19
|
+
try:
|
|
20
|
+
tree = parse_nginx_config(config_path)
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
23
|
+
return
|
|
24
|
+
except Exception as e:
|
|
25
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
26
|
+
return
|
|
20
27
|
routes = []
|
|
21
28
|
# Для каждого server/location строим маршрут
|
|
22
29
|
def walk(d, chain, upstreams):
|
|
@@ -59,17 +66,61 @@ def graph(
|
|
|
59
66
|
console.print("[yellow]Не найдено ни одного маршрута[/yellow]")
|
|
60
67
|
return
|
|
61
68
|
# Красивый вывод
|
|
69
|
+
seen = set()
|
|
62
70
|
for route in routes:
|
|
71
|
+
if not route or route[0][0] != 'server':
|
|
72
|
+
continue
|
|
73
|
+
key = tuple(route)
|
|
74
|
+
if key in seen:
|
|
75
|
+
continue
|
|
76
|
+
seen.add(key)
|
|
63
77
|
t = Text()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
# Получаем label для server
|
|
79
|
+
server_val = route[0][1]
|
|
80
|
+
server_block = None
|
|
81
|
+
# Найти сам блок server по arg
|
|
82
|
+
for d in tree.directives:
|
|
83
|
+
if d.get('block') == 'server' and (d.get('arg','') == server_val or (not d.get('arg') and (server_val == '[no arg]' or not server_val))):
|
|
84
|
+
server_block = d
|
|
85
|
+
break
|
|
86
|
+
label = get_server_label(server_block) if server_block else (server_val or 'default')
|
|
87
|
+
t.append(f"[", style="white")
|
|
88
|
+
t.append(f"server: {label}", style="bold blue")
|
|
89
|
+
t.append("]", style="white")
|
|
90
|
+
for i, (typ, val) in enumerate(route[1:]):
|
|
91
|
+
if typ == 'location':
|
|
92
|
+
t.append(f" -> [", style="white")
|
|
93
|
+
t.append(f"location: {val}", style="yellow")
|
|
94
|
+
t.append("]", style="white")
|
|
69
95
|
elif typ == 'proxy_pass':
|
|
70
96
|
t.append(f" -> proxy_pass: {val}", style="green")
|
|
71
97
|
elif typ == 'upstream':
|
|
72
|
-
t.append(f" ->
|
|
98
|
+
t.append(f" -> [", style="white")
|
|
99
|
+
t.append(f"upstream: {val}", style="magenta")
|
|
100
|
+
t.append("]", style="white")
|
|
73
101
|
elif typ == 'upstream_server':
|
|
74
|
-
t.append(f" ->
|
|
75
|
-
|
|
102
|
+
t.append(f" -> [", style="white")
|
|
103
|
+
t.append(f"server: {val}", style="grey50")
|
|
104
|
+
t.append("]", style="white")
|
|
105
|
+
console.print(t)
|
|
106
|
+
|
|
107
|
+
def get_server_label(server_block):
|
|
108
|
+
arg = server_block.get('arg')
|
|
109
|
+
if arg and arg != '[no arg]':
|
|
110
|
+
return arg
|
|
111
|
+
# Ищем server_name
|
|
112
|
+
names = []
|
|
113
|
+
listens = []
|
|
114
|
+
for sub in server_block.get('directives', []):
|
|
115
|
+
if sub.get('directive') == 'server_name':
|
|
116
|
+
names += sub.get('args', '').split()
|
|
117
|
+
if sub.get('directive') == 'listen':
|
|
118
|
+
listens.append(sub.get('args', ''))
|
|
119
|
+
if names:
|
|
120
|
+
return ' '.join(names)
|
|
121
|
+
if listens:
|
|
122
|
+
return ','.join(listens)
|
|
123
|
+
return 'default'
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
app()
|
|
@@ -19,7 +19,14 @@ def health(
|
|
|
19
19
|
nginx-lens health /etc/nginx/nginx.conf
|
|
20
20
|
nginx-lens health /etc/nginx/nginx.conf --timeout 5 --retries 3
|
|
21
21
|
"""
|
|
22
|
-
|
|
22
|
+
try:
|
|
23
|
+
tree = parse_nginx_config(config_path)
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
26
|
+
return
|
|
27
|
+
except Exception as e:
|
|
28
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
29
|
+
return
|
|
23
30
|
upstreams = tree.get_upstreams()
|
|
24
31
|
results = check_upstreams(upstreams, timeout=timeout, retries=retries)
|
|
25
32
|
table = Table(show_header=True, header_style="bold blue")
|
|
@@ -18,7 +18,14 @@ def include_tree(
|
|
|
18
18
|
nginx-lens include-tree /etc/nginx/nginx.conf
|
|
19
19
|
nginx-lens include-tree /etc/nginx/nginx.conf --directive server_name
|
|
20
20
|
"""
|
|
21
|
-
|
|
21
|
+
try:
|
|
22
|
+
tree = build_include_tree(config_path)
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
25
|
+
return
|
|
26
|
+
except Exception as e:
|
|
27
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
28
|
+
return
|
|
22
29
|
rich_tree = Tree(f"[bold blue]{config_path}[/bold blue]")
|
|
23
30
|
def _add(node, t):
|
|
24
31
|
for k, v in t.items():
|
|
@@ -26,28 +26,36 @@ def logs(
|
|
|
26
26
|
Пример:
|
|
27
27
|
nginx-lens logs /var/log/nginx/access.log --top 20
|
|
28
28
|
"""
|
|
29
|
+
try:
|
|
30
|
+
with open(log_path) as f:
|
|
31
|
+
lines = list(f)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
console.print(f"[red]Файл {log_path} не найден. Проверьте путь к логу.[/red]")
|
|
34
|
+
return
|
|
35
|
+
except Exception as e:
|
|
36
|
+
console.print(f"[red]Ошибка при чтении {log_path}: {e}[/red]")
|
|
37
|
+
return
|
|
29
38
|
status_counter = Counter()
|
|
30
39
|
path_counter = Counter()
|
|
31
40
|
ip_counter = Counter()
|
|
32
41
|
user_agent_counter = Counter()
|
|
33
42
|
errors = defaultdict(list)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
user_agent_counter[ua] += 1
|
|
43
|
+
for line in lines:
|
|
44
|
+
m = log_line_re.search(line)
|
|
45
|
+
if m:
|
|
46
|
+
ip = m.group('ip')
|
|
47
|
+
path = m.group('path')
|
|
48
|
+
status = m.group('status')
|
|
49
|
+
status_counter[status] += 1
|
|
50
|
+
path_counter[path] += 1
|
|
51
|
+
ip_counter[ip] += 1
|
|
52
|
+
if status.startswith('4') or status.startswith('5'):
|
|
53
|
+
errors[status].append(path)
|
|
54
|
+
# user-agent (если есть)
|
|
55
|
+
if '" "' in line:
|
|
56
|
+
ua = line.rsplit('" "', 1)[-1].strip().strip('"')
|
|
57
|
+
if ua:
|
|
58
|
+
user_agent_counter[ua] += 1
|
|
51
59
|
# Топ статусов
|
|
52
60
|
table = Table(title="Top HTTP Status Codes", show_header=True, header_style="bold blue")
|
|
53
61
|
table.add_column("Status")
|
|
@@ -6,19 +6,22 @@ from parser.nginx_parser import parse_nginx_config
|
|
|
6
6
|
import glob
|
|
7
7
|
import os
|
|
8
8
|
|
|
9
|
-
app = typer.Typer()
|
|
9
|
+
app = typer.Typer(help="Показывает, какой server/location обслуживает указанный URL. По умолчанию ищет во всех .conf в /etc/nginx/. Для кастомного пути используйте -c/--config.")
|
|
10
10
|
console = Console()
|
|
11
11
|
|
|
12
12
|
def route(
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)"),
|
|
14
|
+
config_path: str = typer.Option(None, "-c", "--config", help="Путь к кастомному nginx.conf (если не указан — поиск по всем .conf в /etc/nginx)")
|
|
15
15
|
):
|
|
16
16
|
"""
|
|
17
17
|
Показывает, какой server/location обслуживает указанный URL.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
По умолчанию ищет во всех .conf в /etc/nginx/.
|
|
20
|
+
Для кастомного пути используйте опцию -c/--config.
|
|
21
|
+
|
|
22
|
+
Примеры:
|
|
21
23
|
nginx-lens route http://example.com/api/v1
|
|
24
|
+
nginx-lens route -c /etc/nginx/nginx.conf http://example.com/api/v1
|
|
22
25
|
"""
|
|
23
26
|
configs = []
|
|
24
27
|
if config_path:
|
|
@@ -31,8 +34,12 @@ def route(
|
|
|
31
34
|
for conf in configs:
|
|
32
35
|
try:
|
|
33
36
|
tree = parse_nginx_config(conf)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
console.print(f"[red]Файл {conf} не найден. Проверьте путь к конфигу.[/red]")
|
|
39
|
+
continue
|
|
34
40
|
except Exception as e:
|
|
35
|
-
|
|
41
|
+
console.print(f"[red]Ошибка при разборе {conf}: {e}[/red]")
|
|
42
|
+
continue
|
|
36
43
|
res = find_route(tree, url)
|
|
37
44
|
if res:
|
|
38
45
|
server = res['server']
|
|
@@ -33,7 +33,12 @@ def syntax(
|
|
|
33
33
|
if not config_path:
|
|
34
34
|
console.print("[red]Не удалось найти nginx.conf. Укажите путь через -c.[/red]")
|
|
35
35
|
return
|
|
36
|
+
if not os.path.isfile(config_path):
|
|
37
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
38
|
+
return
|
|
36
39
|
cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
|
|
40
|
+
if hasattr(os, 'geteuid') and os.geteuid() != 0:
|
|
41
|
+
cmd = ["sudo"] + cmd
|
|
37
42
|
try:
|
|
38
43
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
39
44
|
if result.returncode == 0:
|
|
@@ -41,8 +46,8 @@ def syntax(
|
|
|
41
46
|
return
|
|
42
47
|
else:
|
|
43
48
|
console.print("[red]Ошибка синтаксиса![/red]")
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
console.print(result.stdout)
|
|
50
|
+
console.print(result.stderr)
|
|
46
51
|
# Парсим все ошибки
|
|
47
52
|
err = result.stderr or result.stdout
|
|
48
53
|
errors = list(ERRORS_RE.finditer(err))
|
|
@@ -34,7 +34,14 @@ def tree(
|
|
|
34
34
|
nginx-lens tree /etc/nginx/nginx.conf --markdown
|
|
35
35
|
nginx-lens tree /etc/nginx/nginx.conf --html
|
|
36
36
|
"""
|
|
37
|
-
|
|
37
|
+
try:
|
|
38
|
+
tree_obj = parse_nginx_config(config_path)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
console.print(f"[red]Файл {config_path} не найден. Проверьте путь к конфигу.[/red]")
|
|
41
|
+
return
|
|
42
|
+
except Exception as e:
|
|
43
|
+
console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
|
|
44
|
+
return
|
|
38
45
|
root = RichTree(f"[bold blue]nginx.conf[/bold blue]")
|
|
39
46
|
_build_tree(tree_obj.directives, root)
|
|
40
47
|
if markdown:
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nginx-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx
|
|
5
5
|
Author: Daniil Astrouski
|
|
6
6
|
Author-email: shelovesuastra@gmail.com
|
|
7
7
|
Requires-Python: >=3.8
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Requires-Dist: typer[all]>=0.9.0
|
|
9
10
|
Requires-Dist: rich>=13.0.0
|
|
10
11
|
Requires-Dist: requests>=2.25.0
|
|
11
12
|
Dynamic: author
|
|
12
13
|
Dynamic: author-email
|
|
14
|
+
Dynamic: license-file
|
|
13
15
|
Dynamic: requires-dist
|
|
14
16
|
Dynamic: requires-python
|
|
15
17
|
Dynamic: summary
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="nginx-lens",
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.7",
|
|
6
6
|
description="CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx",
|
|
7
7
|
author="Daniil Astrouski",
|
|
8
8
|
author_email="shelovesuastra@gmail.com",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
from analyzer.diff import diff_trees
|
|
5
|
-
from parser.nginx_parser import parse_nginx_config
|
|
6
|
-
|
|
7
|
-
app = typer.Typer()
|
|
8
|
-
console = Console()
|
|
9
|
-
|
|
10
|
-
def diff(
|
|
11
|
-
config1: str = typer.Argument(..., help="Первый nginx.conf"),
|
|
12
|
-
config2: str = typer.Argument(..., help="Второй nginx.conf")
|
|
13
|
-
):
|
|
14
|
-
"""
|
|
15
|
-
Сравнивает две конфигурации Nginx и выводит отличия side-by-side.
|
|
16
|
-
|
|
17
|
-
Пример:
|
|
18
|
-
nginx-lens diff /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
|
|
19
|
-
"""
|
|
20
|
-
tree1 = parse_nginx_config(config1)
|
|
21
|
-
tree2 = parse_nginx_config(config2)
|
|
22
|
-
diffs = diff_trees(tree1, tree2)
|
|
23
|
-
if not diffs:
|
|
24
|
-
console.print("[green]Конфигурации идентичны[/green]")
|
|
25
|
-
return
|
|
26
|
-
table = Table(show_header=True, header_style="bold blue")
|
|
27
|
-
table.add_column("Config 1", style="red")
|
|
28
|
-
table.add_column("Config 2", style="green")
|
|
29
|
-
for d in diffs:
|
|
30
|
-
path = "/".join(d['path'])
|
|
31
|
-
if d['type'] == 'added':
|
|
32
|
-
table.add_row("", f"+ {path}")
|
|
33
|
-
elif d['type'] == 'removed':
|
|
34
|
-
table.add_row(f"- {path}", "")
|
|
35
|
-
elif d['type'] == 'changed':
|
|
36
|
-
v1 = str(d['value1'])
|
|
37
|
-
v2 = str(d['value2'])
|
|
38
|
-
table.add_row(f"! {path}\n{v1}", f"! {path}\n{v2}")
|
|
39
|
-
console.print(table)
|
|
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
|