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.
Files changed (49) hide show
  1. {nginx_lens-0.3.1/nginx_lens.egg-info → nginx_lens-0.3.3}/PKG-INFO +2 -1
  2. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/health.py +17 -3
  3. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/resolve.py +14 -4
  4. {nginx_lens-0.3.1 → nginx_lens-0.3.3/nginx_lens.egg-info}/PKG-INFO +2 -1
  5. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/requires.txt +1 -0
  6. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/setup.py +2 -1
  7. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/upstream_checker/checker.py +96 -18
  8. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/LICENSE +0 -0
  9. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/README.md +0 -0
  10. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/__init__.py +0 -0
  11. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/base.py +0 -0
  12. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/conflicts.py +0 -0
  13. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/dead_locations.py +0 -0
  14. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/diff.py +0 -0
  15. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/duplicates.py +0 -0
  16. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/empty_blocks.py +0 -0
  17. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/include.py +0 -0
  18. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/rewrite.py +0 -0
  19. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/route.py +0 -0
  20. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/unused.py +0 -0
  21. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/analyzer/warnings.py +0 -0
  22. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/__init__.py +0 -0
  23. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/analyze.py +0 -0
  24. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/cli.py +0 -0
  25. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/diff.py +0 -0
  26. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/graph.py +0 -0
  27. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/include.py +0 -0
  28. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/logs.py +0 -0
  29. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/route.py +0 -0
  30. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/syntax.py +0 -0
  31. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/commands/tree.py +0 -0
  32. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/__init__.py +0 -0
  33. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/graph.py +0 -0
  34. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/html.py +0 -0
  35. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/exporter/markdown.py +0 -0
  36. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/SOURCES.txt +0 -0
  37. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/dependency_links.txt +0 -0
  38. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/entry_points.txt +0 -0
  39. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/nginx_lens.egg-info/top_level.txt +0 -0
  40. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/parser/__init__.py +0 -0
  41. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/parser/nginx_parser.py +0 -0
  42. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/pyproject.toml +0 -0
  43. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/setup.cfg +0 -0
  44. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_conflicts.py +0 -0
  45. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_duplicates.py +0 -0
  46. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_empty_blocks.py +0 -0
  47. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_health.py +0 -0
  48. {nginx_lens-0.3.1 → nginx_lens-0.3.3}/tests/test_parser.py +0 -0
  49. {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.1
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
- return
32
+ sys.exit(1)
30
33
  except Exception as e:
31
34
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
32
- return
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
- table.add_row(srv["address"], f"[{color}]{status}[/{color}]", f"[green]{resolved_str}[/green]")
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
- return
26
+ sys.exit(1)
24
27
  except Exception as e:
25
28
  console.print(f"[red]Ошибка при разборе {config_path}: {e}[/red]")
26
- return
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
- return
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
- table.add_row(upstream_name, srv["address"], f"[green]{resolved_str}[/green]")
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.1
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
1
  typer[all]>=0.9.0
2
2
  rich>=13.0.0
3
3
  requests>=2.25.0
4
+ dnspython>=2.0.0
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="nginx-lens",
5
- version="0.3.1",
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
- Список IP-адресов в формате "ip:port" или пустой список, если резолвинг не удался
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
- # Пытаемся резолвить DNS имя - получаем все IP-адреса
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
- # gethostbyname_ex возвращает (hostname, aliaslist, ipaddrlist)
92
- _, _, ipaddrlist = socket.gethostbyname_ex(host)
93
- # Фильтруем только IPv4 адреса (IPv6 обрабатываются отдельно)
94
- resolved_ips = []
95
- for ip in ipaddrlist:
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
- # Проверяем, что это IPv4
98
- socket.inet_aton(ip)
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
- except socket.error:
101
- pass
102
- return resolved_ips if resolved_ips else []
103
- except (socket.gaierror, OSError):
104
- return []
105
- except (ValueError, IndexError, AttributeError):
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