nginx-lens 0.3.0__py3-none-any.whl → 0.3.2__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/health.py CHANGED
@@ -51,15 +51,21 @@ def health(
51
51
  color = "green" if srv["healthy"] else "red"
52
52
 
53
53
  if resolve:
54
- resolved = None
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
- resolved = resolved_srv["resolved"]
58
+ resolved_list = resolved_srv["resolved"]
59
59
  break
60
60
 
61
- if resolved:
62
- table.add_row(srv["address"], f"[{color}]{status}[/{color}]", resolved)
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:
commands/resolve.py CHANGED
@@ -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
- if srv["resolved"]:
44
- table.add_row(upstream_name, srv["address"], f"[green]{srv['resolved']}[/green]")
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.0
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
@@ -15,10 +15,10 @@ commands/analyze.py,sha256=W6begSgXNjgKJGoGeguR3WKgHPLkClWXxxpDcqvsJdc,8343
15
15
  commands/cli.py,sha256=brzp6xDDWIrm7ibaoT4x94hgAdBB2DVWniXoK8dRylE,782
16
16
  commands/diff.py,sha256=C7gRIWh6DNWHzjiQBPVTn-rZ40m2KCY75Zd6Q4URJIE,2076
17
17
  commands/graph.py,sha256=xB6KjXBkLmm5gII3e-5BMRGO7WeTwc7EFxRGzYgnme4,5947
18
- commands/health.py,sha256=Q_eDWrDin-wYzxCCDDfVkQd5VzFjUC0e9I7opGyaSqw,2897
18
+ commands/health.py,sha256=JAlUKx2kXhi2eigSQXG7GxuQMPydYzojgV2qaRcoLWw,3429
19
19
  commands/include.py,sha256=5PTYG5C00-AlWfIgpQXLq9E7C9yTFSv7HrZkM5ogDps,2224
20
20
  commands/logs.py,sha256=RkPUdIpbO9dOVL56lemreYRuAjMjcqqMxRCKOFv2gC4,3691
21
- commands/resolve.py,sha256=u55MmHRNLIWJpQcPjSOjxtOvsv3Nf0rOQwkh122xyvM,1658
21
+ commands/resolve.py,sha256=LVfp-cva-XlKOOIS5Da908mBGgjq1DtZlsahMUylAsk,2151
22
22
  commands/route.py,sha256=-x_71u6ENl3iO-oxK3bdE8v5eZKf4xRCydeUyXMFVrY,3163
23
23
  commands/syntax.py,sha256=ZWFdaL8LVv9S694wlk2aV3HJKb0OSKjw3wNgTlNvFR8,3418
24
24
  commands/tree.py,sha256=mDfx0Aeg1EDQSYQoJ2nJIkSd_uP7ZR7pEqy7Cw3clQ0,2161
@@ -26,13 +26,13 @@ exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
27
27
  exporter/html.py,sha256=uquEM-WvBt2aV9GshgaI3UVhYd8sD0QQ-OmuNtvYUdU,798
28
28
  exporter/markdown.py,sha256=_0mXQIhurGEZ0dO-eq9DbsuKNrgEDIblgtL3DAgYNo8,724
29
- nginx_lens-0.3.0.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
29
+ nginx_lens-0.3.2.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
30
30
  parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  parser/nginx_parser.py,sha256=Sa9FtGAkvTqNzoehBvgLUWPJHLLIZYWH9ugSHW50X8s,3699
32
32
  upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- upstream_checker/checker.py,sha256=816yjsYlsyjs3B5Xmj7yjU1d-2xesTnMxhCR9WX4tsU,4409
34
- nginx_lens-0.3.0.dist-info/METADATA,sha256=vZFN0kAKjwFK9LBeX0U6MjPXtx7l-e7E7CJIEXn_tE0,520
35
- nginx_lens-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- nginx_lens-0.3.0.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
37
- nginx_lens-0.3.0.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
38
- nginx_lens-0.3.0.dist-info/RECORD,,
33
+ upstream_checker/checker.py,sha256=ZwAlIW6yh0OVdeI44dpZUeSUbByE04gBxTUsRMRUgU8,9496
34
+ nginx_lens-0.3.2.dist-info/METADATA,sha256=DanBz8o77g84wsKtgPzMKCa2IrX8VVrq78DOFuJeTdA,552
35
+ nginx_lens-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
+ nginx_lens-0.3.2.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
37
+ nginx_lens-0.3.2.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
38
+ nginx_lens-0.3.2.dist-info/RECORD,,
@@ -3,7 +3,13 @@
3
3
  import socket
4
4
  import time
5
5
  import http.client
6
- from typing import Dict, List, Optional
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:
@@ -49,48 +55,146 @@ def check_http(address: str, timeout: float, retries: int) -> bool:
49
55
  return False
50
56
 
51
57
 
52
- def resolve_address(address: str) -> Optional[str]:
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" или None, если резолвинг не удался
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]
64
74
 
65
75
  if ":" not in host_port:
66
- return None
76
+ return []
67
77
 
68
78
  parts = host_port.rsplit(":", 1)
69
79
  if len(parts) != 2:
70
- return None
80
+ return []
71
81
  host, port = parts
72
82
 
83
+ # Если это уже IPv4 адрес, возвращаем как есть
73
84
  try:
74
85
  socket.inet_aton(host)
75
- return host_port
86
+ return [host_port]
76
87
  except socket.error:
77
88
  pass
78
89
 
90
+ # Проверяем IPv6 (в квадратных скобках)
79
91
  if host.startswith("[") and host.endswith("]"):
80
92
  ipv6_host = host[1:-1]
81
93
  try:
82
94
  socket.inet_pton(socket.AF_INET6, ipv6_host)
83
- return host_port
95
+ return [host_port]
84
96
  except (socket.error, OSError):
85
97
  pass
86
98
 
87
- try:
88
- ip = socket.gethostbyname(host)
89
- return f"{ip}:{port}"
90
- except (socket.gaierror, OSError):
91
- return None
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)
92
105
  except (ValueError, IndexError, AttributeError):
93
- return None
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 []
94
198
 
95
199
 
96
200
  def resolve_upstreams(
@@ -102,9 +206,9 @@ def resolve_upstreams(
102
206
  Возвращает:
103
207
  {
104
208
  "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},
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": []},
108
212
  ...
109
213
  ]
110
214
  }