proxy2vpn 0.2.0__tar.gz → 0.3.0__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.
- {proxy2vpn-0.2.0/src/proxy2vpn.egg-info → proxy2vpn-0.3.0}/PKG-INFO +14 -20
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/README.md +13 -19
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/pyproject.toml +1 -1
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/cli.py +263 -52
- proxy2vpn-0.3.0/src/proxy2vpn/diagnostics.py +100 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/docker_ops.py +45 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/typer_ext.py +5 -5
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0/src/proxy2vpn.egg-info}/PKG-INFO +14 -20
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn.egg-info/SOURCES.txt +3 -1
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_init_command.py +3 -3
- proxy2vpn-0.3.0/tests/test_version_option.py +31 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/LICENSE +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/setup.cfg +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/__init__.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/__main__.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/compose_manager.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/compose_utils.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/compose_validator.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/config.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/models.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/preset_manager.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn/server_manager.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn.egg-info/dependency_links.txt +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn.egg-info/entry_points.txt +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn.egg-info/requires.txt +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/src/proxy2vpn.egg-info/top_level.txt +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_cli_entrypoint.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_compose_manager.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_compose_utils.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_compose_validator.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_docker_ops.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_preset_manager.py +0 -0
- {proxy2vpn-0.2.0 → proxy2vpn-0.3.0}/tests/test_server_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy2vpn
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Proxy2VPN Python utilities
|
|
5
5
|
Author-email: Serhii Khalymon <serhii.khalymon@pm.me>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Python command-line interface for managing multiple VPN containers with Docker.
|
|
|
25
25
|
## Features
|
|
26
26
|
- Manage VPN credentials as reusable profiles
|
|
27
27
|
- Create and control VPN services
|
|
28
|
-
-
|
|
28
|
+
- Multi-service control with `--all` flags
|
|
29
29
|
- Query and validate provider server locations
|
|
30
30
|
- Apply predefined presets for common setups
|
|
31
31
|
|
|
@@ -61,7 +61,7 @@ proxy2vpn --help
|
|
|
61
61
|
## Quick Start
|
|
62
62
|
1. Initialize the compose file:
|
|
63
63
|
```bash
|
|
64
|
-
proxy2vpn init
|
|
64
|
+
proxy2vpn system init
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
2. Create a profile file with your VPN credentials:
|
|
@@ -87,13 +87,15 @@ proxy2vpn --help
|
|
|
87
87
|
5. View status and test connectivity:
|
|
88
88
|
```bash
|
|
89
89
|
proxy2vpn vpn list
|
|
90
|
-
proxy2vpn test vpn1
|
|
90
|
+
proxy2vpn vpn test vpn1
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
## Command overview
|
|
94
94
|
|
|
95
|
-
###
|
|
96
|
-
- `proxy2vpn init [--force]`
|
|
95
|
+
### System operations
|
|
96
|
+
- `proxy2vpn system init [--force]`
|
|
97
|
+
- `proxy2vpn system validate`
|
|
98
|
+
- `proxy2vpn system diagnose [--lines N] [--all] [--verbose] [--json]`
|
|
97
99
|
|
|
98
100
|
### Profiles
|
|
99
101
|
- `proxy2vpn profile create NAME ENV_FILE`
|
|
@@ -102,18 +104,13 @@ proxy2vpn --help
|
|
|
102
104
|
|
|
103
105
|
### VPN services
|
|
104
106
|
- `proxy2vpn vpn create NAME PROFILE [--port PORT] [--provider PROVIDER] [--location LOCATION]`
|
|
105
|
-
- `proxy2vpn vpn list`
|
|
106
|
-
- `proxy2vpn vpn start NAME`
|
|
107
|
-
- `proxy2vpn vpn stop NAME`
|
|
108
|
-
- `proxy2vpn vpn restart NAME`
|
|
107
|
+
- `proxy2vpn vpn list [--diagnose] [--ips-only]`
|
|
108
|
+
- `proxy2vpn vpn start [NAME | --all]`
|
|
109
|
+
- `proxy2vpn vpn stop [NAME | --all]`
|
|
110
|
+
- `proxy2vpn vpn restart [NAME | --all]`
|
|
109
111
|
- `proxy2vpn vpn logs NAME [--lines N] [--follow]`
|
|
110
|
-
- `proxy2vpn vpn delete NAME`
|
|
111
|
-
|
|
112
|
-
### Bulk operations
|
|
113
|
-
- `proxy2vpn bulk up`
|
|
114
|
-
- `proxy2vpn bulk down`
|
|
115
|
-
- `proxy2vpn bulk status`
|
|
116
|
-
- `proxy2vpn bulk ips`
|
|
112
|
+
- `proxy2vpn vpn delete [NAME | --all]`
|
|
113
|
+
- `proxy2vpn vpn test NAME`
|
|
117
114
|
|
|
118
115
|
### Server database
|
|
119
116
|
- `proxy2vpn servers update`
|
|
@@ -126,9 +123,6 @@ proxy2vpn --help
|
|
|
126
123
|
- `proxy2vpn preset list`
|
|
127
124
|
- `proxy2vpn preset apply PRESET SERVICE [--port PORT]`
|
|
128
125
|
|
|
129
|
-
### Testing
|
|
130
|
-
- `proxy2vpn test SERVICE` – verify that a proxy container is reachable
|
|
131
|
-
|
|
132
126
|
## Development
|
|
133
127
|
|
|
134
128
|
### Setup
|
|
@@ -5,7 +5,7 @@ Python command-line interface for managing multiple VPN containers with Docker.
|
|
|
5
5
|
## Features
|
|
6
6
|
- Manage VPN credentials as reusable profiles
|
|
7
7
|
- Create and control VPN services
|
|
8
|
-
-
|
|
8
|
+
- Multi-service control with `--all` flags
|
|
9
9
|
- Query and validate provider server locations
|
|
10
10
|
- Apply predefined presets for common setups
|
|
11
11
|
|
|
@@ -41,7 +41,7 @@ proxy2vpn --help
|
|
|
41
41
|
## Quick Start
|
|
42
42
|
1. Initialize the compose file:
|
|
43
43
|
```bash
|
|
44
|
-
proxy2vpn init
|
|
44
|
+
proxy2vpn system init
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
2. Create a profile file with your VPN credentials:
|
|
@@ -67,13 +67,15 @@ proxy2vpn --help
|
|
|
67
67
|
5. View status and test connectivity:
|
|
68
68
|
```bash
|
|
69
69
|
proxy2vpn vpn list
|
|
70
|
-
proxy2vpn test vpn1
|
|
70
|
+
proxy2vpn vpn test vpn1
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
## Command overview
|
|
74
74
|
|
|
75
|
-
###
|
|
76
|
-
- `proxy2vpn init [--force]`
|
|
75
|
+
### System operations
|
|
76
|
+
- `proxy2vpn system init [--force]`
|
|
77
|
+
- `proxy2vpn system validate`
|
|
78
|
+
- `proxy2vpn system diagnose [--lines N] [--all] [--verbose] [--json]`
|
|
77
79
|
|
|
78
80
|
### Profiles
|
|
79
81
|
- `proxy2vpn profile create NAME ENV_FILE`
|
|
@@ -82,18 +84,13 @@ proxy2vpn --help
|
|
|
82
84
|
|
|
83
85
|
### VPN services
|
|
84
86
|
- `proxy2vpn vpn create NAME PROFILE [--port PORT] [--provider PROVIDER] [--location LOCATION]`
|
|
85
|
-
- `proxy2vpn vpn list`
|
|
86
|
-
- `proxy2vpn vpn start NAME`
|
|
87
|
-
- `proxy2vpn vpn stop NAME`
|
|
88
|
-
- `proxy2vpn vpn restart NAME`
|
|
87
|
+
- `proxy2vpn vpn list [--diagnose] [--ips-only]`
|
|
88
|
+
- `proxy2vpn vpn start [NAME | --all]`
|
|
89
|
+
- `proxy2vpn vpn stop [NAME | --all]`
|
|
90
|
+
- `proxy2vpn vpn restart [NAME | --all]`
|
|
89
91
|
- `proxy2vpn vpn logs NAME [--lines N] [--follow]`
|
|
90
|
-
- `proxy2vpn vpn delete NAME`
|
|
91
|
-
|
|
92
|
-
### Bulk operations
|
|
93
|
-
- `proxy2vpn bulk up`
|
|
94
|
-
- `proxy2vpn bulk down`
|
|
95
|
-
- `proxy2vpn bulk status`
|
|
96
|
-
- `proxy2vpn bulk ips`
|
|
92
|
+
- `proxy2vpn vpn delete [NAME | --all]`
|
|
93
|
+
- `proxy2vpn vpn test NAME`
|
|
97
94
|
|
|
98
95
|
### Server database
|
|
99
96
|
- `proxy2vpn servers update`
|
|
@@ -106,9 +103,6 @@ proxy2vpn --help
|
|
|
106
103
|
- `proxy2vpn preset list`
|
|
107
104
|
- `proxy2vpn preset apply PRESET SERVICE [--port PORT]`
|
|
108
105
|
|
|
109
|
-
### Testing
|
|
110
|
-
- `proxy2vpn test SERVICE` – verify that a proxy container is reachable
|
|
111
|
-
|
|
112
106
|
## Development
|
|
113
107
|
|
|
114
108
|
### Setup
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
import json
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
from .typer_ext import HelpfulTyper
|
|
@@ -19,35 +20,50 @@ app = HelpfulTyper(help="proxy2vpn command line interface")
|
|
|
19
20
|
profile_app = HelpfulTyper(help="Manage VPN profiles")
|
|
20
21
|
vpn_app = HelpfulTyper(help="Manage VPN services")
|
|
21
22
|
server_app = HelpfulTyper(help="Manage cached server lists")
|
|
23
|
+
system_app = HelpfulTyper(help="System level operations")
|
|
22
24
|
bulk_app = HelpfulTyper(help="Bulk container operations")
|
|
23
25
|
preset_app = HelpfulTyper(help="Manage presets")
|
|
24
26
|
|
|
25
27
|
app.add_typer(profile_app, name="profile")
|
|
26
28
|
app.add_typer(vpn_app, name="vpn")
|
|
27
29
|
app.add_typer(server_app, name="servers")
|
|
28
|
-
app.add_typer(
|
|
30
|
+
app.add_typer(system_app, name="system")
|
|
29
31
|
app.add_typer(preset_app, name="preset")
|
|
32
|
+
app.add_typer(bulk_app, name="bulk", hidden=True)
|
|
30
33
|
|
|
31
34
|
|
|
32
|
-
@app.callback()
|
|
35
|
+
@app.callback(invoke_without_command=True)
|
|
33
36
|
def main(
|
|
34
37
|
ctx: typer.Context,
|
|
35
38
|
compose_file: Path = typer.Option(
|
|
36
39
|
config.COMPOSE_FILE, "--compose-file", "-f", help="Path to compose file"
|
|
37
40
|
),
|
|
41
|
+
version: bool = typer.Option(
|
|
42
|
+
False,
|
|
43
|
+
"--version",
|
|
44
|
+
"-V",
|
|
45
|
+
help="Show version and exit",
|
|
46
|
+
is_eager=True,
|
|
47
|
+
),
|
|
38
48
|
):
|
|
39
49
|
"""Store global options in context."""
|
|
50
|
+
if version:
|
|
51
|
+
from . import __version__
|
|
52
|
+
|
|
53
|
+
typer.echo(__version__)
|
|
54
|
+
raise typer.Exit()
|
|
55
|
+
|
|
40
56
|
ctx.obj = ctx.obj or {}
|
|
41
57
|
ctx.obj["compose_file"] = compose_file
|
|
42
58
|
|
|
43
59
|
|
|
44
60
|
# ---------------------------------------------------------------------------
|
|
45
|
-
#
|
|
61
|
+
# System commands
|
|
46
62
|
# ---------------------------------------------------------------------------
|
|
47
63
|
|
|
48
64
|
|
|
49
|
-
@
|
|
50
|
-
def
|
|
65
|
+
@system_app.command("init")
|
|
66
|
+
def system_init(
|
|
51
67
|
ctx: typer.Context,
|
|
52
68
|
force: bool = typer.Option(
|
|
53
69
|
False, "--force", "-f", help="Overwrite existing compose file if it exists"
|
|
@@ -147,44 +163,96 @@ def vpn_create(
|
|
|
147
163
|
|
|
148
164
|
|
|
149
165
|
@vpn_app.command("list")
|
|
150
|
-
def vpn_list(
|
|
166
|
+
def vpn_list(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
diagnose: bool = typer.Option(
|
|
169
|
+
False, "--diagnose", help="Include diagnostic health scores"
|
|
170
|
+
),
|
|
171
|
+
ips_only: bool = typer.Option(
|
|
172
|
+
False, "--ips-only", help="Show only container IP addresses"
|
|
173
|
+
),
|
|
174
|
+
):
|
|
151
175
|
"""List VPN services with their status and IP addresses."""
|
|
152
176
|
|
|
153
177
|
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
154
178
|
manager = ComposeManager(compose_file)
|
|
155
|
-
from .docker_ops import
|
|
179
|
+
from .docker_ops import (
|
|
180
|
+
get_vpn_containers,
|
|
181
|
+
get_container_ip,
|
|
182
|
+
analyze_container_logs,
|
|
183
|
+
)
|
|
184
|
+
from .diagnostics import DiagnosticAnalyzer
|
|
185
|
+
|
|
186
|
+
if ips_only:
|
|
187
|
+
containers = get_vpn_containers(all=False)
|
|
188
|
+
for container in containers:
|
|
189
|
+
ip = get_container_ip(container)
|
|
190
|
+
typer.echo(f"{container.name}: {ip}")
|
|
191
|
+
return
|
|
156
192
|
|
|
157
193
|
services = manager.list_services()
|
|
158
194
|
containers = {c.name: c for c in get_vpn_containers(all=True)}
|
|
195
|
+
analyzer = DiagnosticAnalyzer() if diagnose else None
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
header = f"{'NAME':<15} {'PORT':<8} {'PROFILE':<12} {'STATUS':<10} {'IP':<15}"
|
|
198
|
+
if diagnose:
|
|
199
|
+
header += f" {'HEALTH':<7}"
|
|
200
|
+
typer.echo(header)
|
|
201
|
+
typer.echo("-" * len(header))
|
|
162
202
|
for svc in services:
|
|
163
203
|
container = containers.get(svc.name)
|
|
164
204
|
if container:
|
|
165
205
|
status = container.status
|
|
166
206
|
ip = get_container_ip(container) if status == "running" else "N/A"
|
|
207
|
+
health = "N/A"
|
|
208
|
+
if diagnose:
|
|
209
|
+
results = analyze_container_logs(container.name, analyzer=analyzer)
|
|
210
|
+
health = str(analyzer.health_score(results))
|
|
167
211
|
else:
|
|
168
212
|
status = "not created"
|
|
169
213
|
ip = "N/A"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
health = "N/A"
|
|
215
|
+
line = f"{svc.name:<15} {svc.port:<8} {svc.profile:<12} {status:<10} {ip:<15}"
|
|
216
|
+
if diagnose:
|
|
217
|
+
line += f" {health:<7}"
|
|
218
|
+
typer.echo(line)
|
|
173
219
|
|
|
174
220
|
|
|
175
221
|
@vpn_app.command("start")
|
|
176
|
-
def vpn_start(
|
|
177
|
-
|
|
222
|
+
def vpn_start(
|
|
223
|
+
ctx: typer.Context,
|
|
224
|
+
name: str | None = typer.Argument(None),
|
|
225
|
+
all: bool = typer.Option(False, "--all", help="Start all VPN services"),
|
|
226
|
+
):
|
|
227
|
+
"""Start one or all VPN containers."""
|
|
178
228
|
|
|
179
229
|
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
180
230
|
manager = ComposeManager(compose_file)
|
|
231
|
+
if all and name is not None:
|
|
232
|
+
typer.echo("Cannot specify NAME when using --all", err=True)
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
if all:
|
|
235
|
+
from .docker_ops import start_all_vpn_containers
|
|
236
|
+
|
|
237
|
+
results = start_all_vpn_containers()
|
|
238
|
+
for svc_name, started in results:
|
|
239
|
+
if started:
|
|
240
|
+
typer.echo(f"\u2713 Started {svc_name}")
|
|
241
|
+
else:
|
|
242
|
+
typer.echo(f"\u2192 {svc_name} already running")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if name is None:
|
|
246
|
+
typer.echo("Specify a service NAME or use --all", err=True)
|
|
247
|
+
raise typer.Exit(1)
|
|
181
248
|
try:
|
|
182
249
|
manager.get_service(name)
|
|
183
250
|
except KeyError:
|
|
184
251
|
typer.echo(f"Service '{name}' not found.", err=True)
|
|
185
252
|
raise typer.Exit(1)
|
|
186
253
|
|
|
187
|
-
from .docker_ops import start_container
|
|
254
|
+
from .docker_ops import start_container, analyze_container_logs
|
|
255
|
+
from .diagnostics import DiagnosticAnalyzer
|
|
188
256
|
|
|
189
257
|
try:
|
|
190
258
|
start_container(name)
|
|
@@ -194,22 +262,47 @@ def vpn_start(ctx: typer.Context, name: str):
|
|
|
194
262
|
raise typer.Exit(1)
|
|
195
263
|
except APIError as exc:
|
|
196
264
|
typer.echo(f"Failed to start '{name}': {exc.explanation}", err=True)
|
|
265
|
+
analyzer = DiagnosticAnalyzer()
|
|
266
|
+
results = analyze_container_logs(name, analyzer=analyzer)
|
|
267
|
+
if results:
|
|
268
|
+
typer.echo("Diagnostic hints:", err=True)
|
|
269
|
+
for res in results:
|
|
270
|
+
typer.echo(f" - {res.message}: {res.recommendation}", err=True)
|
|
197
271
|
raise typer.Exit(1)
|
|
198
272
|
|
|
199
273
|
|
|
200
274
|
@vpn_app.command("stop")
|
|
201
|
-
def vpn_stop(
|
|
202
|
-
|
|
275
|
+
def vpn_stop(
|
|
276
|
+
ctx: typer.Context,
|
|
277
|
+
name: str | None = typer.Argument(None),
|
|
278
|
+
all: bool = typer.Option(False, "--all", help="Stop all VPN services"),
|
|
279
|
+
):
|
|
280
|
+
"""Stop one or all VPN containers."""
|
|
203
281
|
|
|
204
282
|
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
205
283
|
manager = ComposeManager(compose_file)
|
|
284
|
+
if all and name is not None:
|
|
285
|
+
typer.echo("Cannot specify NAME when using --all", err=True)
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
if all:
|
|
288
|
+
from .docker_ops import stop_all_vpn_containers
|
|
289
|
+
|
|
290
|
+
results = stop_all_vpn_containers()
|
|
291
|
+
for svc_name in results:
|
|
292
|
+
typer.echo(f"\u2713 Stopped {svc_name}")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if name is None:
|
|
296
|
+
typer.echo("Specify a service NAME or use --all", err=True)
|
|
297
|
+
raise typer.Exit(1)
|
|
206
298
|
try:
|
|
207
299
|
manager.get_service(name)
|
|
208
300
|
except KeyError:
|
|
209
301
|
typer.echo(f"Service '{name}' not found.", err=True)
|
|
210
302
|
raise typer.Exit(1)
|
|
211
303
|
|
|
212
|
-
from .docker_ops import stop_container
|
|
304
|
+
from .docker_ops import stop_container, analyze_container_logs
|
|
305
|
+
from .diagnostics import DiagnosticAnalyzer
|
|
213
306
|
|
|
214
307
|
try:
|
|
215
308
|
stop_container(name)
|
|
@@ -219,22 +312,55 @@ def vpn_stop(ctx: typer.Context, name: str):
|
|
|
219
312
|
raise typer.Exit(1)
|
|
220
313
|
except APIError as exc:
|
|
221
314
|
typer.echo(f"Failed to stop '{name}': {exc.explanation}", err=True)
|
|
315
|
+
analyzer = DiagnosticAnalyzer()
|
|
316
|
+
results = analyze_container_logs(name, analyzer=analyzer)
|
|
317
|
+
if results:
|
|
318
|
+
typer.echo("Diagnostic hints:", err=True)
|
|
319
|
+
for res in results:
|
|
320
|
+
typer.echo(f" - {res.message}: {res.recommendation}", err=True)
|
|
222
321
|
raise typer.Exit(1)
|
|
223
322
|
|
|
224
323
|
|
|
225
324
|
@vpn_app.command("restart")
|
|
226
|
-
def vpn_restart(
|
|
227
|
-
|
|
325
|
+
def vpn_restart(
|
|
326
|
+
ctx: typer.Context,
|
|
327
|
+
name: str | None = typer.Argument(None),
|
|
328
|
+
all: bool = typer.Option(False, "--all", help="Restart all VPN services"),
|
|
329
|
+
):
|
|
330
|
+
"""Restart one or all VPN containers."""
|
|
228
331
|
|
|
229
332
|
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
230
333
|
manager = ComposeManager(compose_file)
|
|
334
|
+
if all and name is not None:
|
|
335
|
+
typer.echo("Cannot specify NAME when using --all", err=True)
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
if all:
|
|
338
|
+
from .docker_ops import get_vpn_containers, restart_container
|
|
339
|
+
|
|
340
|
+
containers = get_vpn_containers(all=True)
|
|
341
|
+
for container in containers:
|
|
342
|
+
try:
|
|
343
|
+
restart_container(container.name)
|
|
344
|
+
typer.echo(f"\u2713 Restarted {container.name}")
|
|
345
|
+
except NotFound:
|
|
346
|
+
typer.echo(f"Container '{container.name}' does not exist.", err=True)
|
|
347
|
+
except APIError as exc:
|
|
348
|
+
typer.echo(
|
|
349
|
+
f"Failed to restart '{container.name}': {exc.explanation}", err=True
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if name is None:
|
|
354
|
+
typer.echo("Specify a service NAME or use --all", err=True)
|
|
355
|
+
raise typer.Exit(1)
|
|
231
356
|
try:
|
|
232
357
|
manager.get_service(name)
|
|
233
358
|
except KeyError:
|
|
234
359
|
typer.echo(f"Service '{name}' not found.", err=True)
|
|
235
360
|
raise typer.Exit(1)
|
|
236
361
|
|
|
237
|
-
from .docker_ops import restart_container
|
|
362
|
+
from .docker_ops import restart_container, analyze_container_logs
|
|
363
|
+
from .diagnostics import DiagnosticAnalyzer
|
|
238
364
|
|
|
239
365
|
try:
|
|
240
366
|
restart_container(name)
|
|
@@ -244,6 +370,12 @@ def vpn_restart(ctx: typer.Context, name: str):
|
|
|
244
370
|
raise typer.Exit(1)
|
|
245
371
|
except APIError as exc:
|
|
246
372
|
typer.echo(f"Failed to restart '{name}': {exc.explanation}", err=True)
|
|
373
|
+
analyzer = DiagnosticAnalyzer()
|
|
374
|
+
results = analyze_container_logs(name, analyzer=analyzer)
|
|
375
|
+
if results:
|
|
376
|
+
typer.echo("Diagnostic hints:", err=True)
|
|
377
|
+
for res in results:
|
|
378
|
+
typer.echo(f" - {res.message}: {res.recommendation}", err=True)
|
|
247
379
|
raise typer.Exit(1)
|
|
248
380
|
|
|
249
381
|
|
|
@@ -277,13 +409,39 @@ def vpn_logs(
|
|
|
277
409
|
@vpn_app.command("delete")
|
|
278
410
|
def vpn_delete(
|
|
279
411
|
ctx: typer.Context,
|
|
280
|
-
name: str,
|
|
412
|
+
name: str | None = typer.Argument(None),
|
|
413
|
+
all: bool = typer.Option(False, "--all", help="Delete all VPN services"),
|
|
281
414
|
force: bool = typer.Option(False, "--force", "-f", help="Do not prompt"),
|
|
282
415
|
):
|
|
283
|
-
"""Delete
|
|
416
|
+
"""Delete one or all VPN services and remove their containers."""
|
|
284
417
|
|
|
285
418
|
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
286
419
|
manager = ComposeManager(compose_file)
|
|
420
|
+
from .docker_ops import remove_container, stop_container
|
|
421
|
+
|
|
422
|
+
if all and name is not None:
|
|
423
|
+
typer.echo("Cannot specify NAME when using --all", err=True)
|
|
424
|
+
raise typer.Exit(1)
|
|
425
|
+
if all:
|
|
426
|
+
services = manager.list_services()
|
|
427
|
+
if not force and not typer.confirm("Delete all services?"):
|
|
428
|
+
raise typer.Exit()
|
|
429
|
+
for svc in services:
|
|
430
|
+
try:
|
|
431
|
+
stop_container(svc.name)
|
|
432
|
+
except NotFound:
|
|
433
|
+
pass
|
|
434
|
+
try:
|
|
435
|
+
remove_container(svc.name)
|
|
436
|
+
except NotFound:
|
|
437
|
+
pass
|
|
438
|
+
manager.remove_service(svc.name)
|
|
439
|
+
typer.echo(f"Service '{svc.name}' deleted.")
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
if name is None:
|
|
443
|
+
typer.echo("Specify a service NAME or use --all", err=True)
|
|
444
|
+
raise typer.Exit(1)
|
|
287
445
|
try:
|
|
288
446
|
manager.get_service(name)
|
|
289
447
|
except KeyError:
|
|
@@ -293,8 +451,6 @@ def vpn_delete(
|
|
|
293
451
|
if not force and not typer.confirm(f"Delete service '{name}'?"):
|
|
294
452
|
raise typer.Exit()
|
|
295
453
|
|
|
296
|
-
from .docker_ops import remove_container, stop_container
|
|
297
|
-
|
|
298
454
|
try:
|
|
299
455
|
stop_container(name)
|
|
300
456
|
except NotFound:
|
|
@@ -308,6 +464,27 @@ def vpn_delete(
|
|
|
308
464
|
typer.echo(f"Service '{name}' deleted.")
|
|
309
465
|
|
|
310
466
|
|
|
467
|
+
@vpn_app.command("test")
|
|
468
|
+
def vpn_test(ctx: typer.Context, name: str):
|
|
469
|
+
"""Test that a VPN service proxy is working."""
|
|
470
|
+
|
|
471
|
+
compose_file: Path = ctx.obj.get("compose_file", config.COMPOSE_FILE)
|
|
472
|
+
manager = ComposeManager(compose_file)
|
|
473
|
+
try:
|
|
474
|
+
manager.get_service(name)
|
|
475
|
+
except KeyError:
|
|
476
|
+
typer.echo(f"Service '{name}' not found.", err=True)
|
|
477
|
+
raise typer.Exit(1)
|
|
478
|
+
|
|
479
|
+
from .docker_ops import test_vpn_connection
|
|
480
|
+
|
|
481
|
+
if test_vpn_connection(name):
|
|
482
|
+
typer.echo("VPN connection is active.")
|
|
483
|
+
else:
|
|
484
|
+
typer.echo("VPN connection failed.", err=True)
|
|
485
|
+
raise typer.Exit(1)
|
|
486
|
+
|
|
487
|
+
|
|
311
488
|
# ---------------------------------------------------------------------------
|
|
312
489
|
# Bulk container commands
|
|
313
490
|
# ---------------------------------------------------------------------------
|
|
@@ -317,6 +494,7 @@ def vpn_delete(
|
|
|
317
494
|
def bulk_up():
|
|
318
495
|
"""Start all VPN containers."""
|
|
319
496
|
|
|
497
|
+
typer.echo("Deprecated: use 'vpn start --all' instead.", err=True)
|
|
320
498
|
from .docker_ops import start_all_vpn_containers
|
|
321
499
|
|
|
322
500
|
results = start_all_vpn_containers()
|
|
@@ -331,6 +509,7 @@ def bulk_up():
|
|
|
331
509
|
def bulk_down():
|
|
332
510
|
"""Stop all running VPN containers."""
|
|
333
511
|
|
|
512
|
+
typer.echo("Deprecated: use 'vpn stop --all' instead.", err=True)
|
|
334
513
|
from .docker_ops import stop_all_vpn_containers
|
|
335
514
|
|
|
336
515
|
results = stop_all_vpn_containers()
|
|
@@ -339,30 +518,19 @@ def bulk_down():
|
|
|
339
518
|
|
|
340
519
|
|
|
341
520
|
@bulk_app.command("status")
|
|
342
|
-
def bulk_status():
|
|
521
|
+
def bulk_status(ctx: typer.Context):
|
|
343
522
|
"""Show status and IP address for VPN containers."""
|
|
344
523
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
containers = get_vpn_containers(all=True)
|
|
348
|
-
typer.echo(f"{'NAME':<15} {'STATUS':<10} {'PORT':<8} {'IP':<15}")
|
|
349
|
-
typer.echo("-" * 50)
|
|
350
|
-
for container in containers:
|
|
351
|
-
port = container.labels.get("vpn.port", "N/A")
|
|
352
|
-
ip = get_container_ip(container) if container.status == "running" else "N/A"
|
|
353
|
-
typer.echo(f"{container.name:<15} {container.status:<10} {port:<8} {ip:<15}")
|
|
524
|
+
typer.echo("Deprecated: use 'vpn list' instead.", err=True)
|
|
525
|
+
vpn_list(ctx)
|
|
354
526
|
|
|
355
527
|
|
|
356
528
|
@bulk_app.command("ips")
|
|
357
|
-
def bulk_ips():
|
|
529
|
+
def bulk_ips(ctx: typer.Context):
|
|
358
530
|
"""Show IP addresses of running VPN containers."""
|
|
359
531
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
containers = get_vpn_containers(all=False)
|
|
363
|
-
for container in containers:
|
|
364
|
-
ip = get_container_ip(container)
|
|
365
|
-
typer.echo(f"{container.name}: {ip}")
|
|
532
|
+
typer.echo("Deprecated: use 'vpn list --ips-only' instead.", err=True)
|
|
533
|
+
vpn_list(ctx, ips_only=True)
|
|
366
534
|
|
|
367
535
|
|
|
368
536
|
# ---------------------------------------------------------------------------
|
|
@@ -454,8 +622,8 @@ def preset_apply(
|
|
|
454
622
|
typer.echo(f"Service '{service}' created from preset '{preset}' on port {port}.")
|
|
455
623
|
|
|
456
624
|
|
|
457
|
-
@
|
|
458
|
-
def
|
|
625
|
+
@system_app.command("validate")
|
|
626
|
+
def system_validate(compose_file: Path = typer.Option(config.COMPOSE_FILE)):
|
|
459
627
|
"""Validate that the compose file is well formed."""
|
|
460
628
|
|
|
461
629
|
errors = validate_compose(compose_file)
|
|
@@ -466,17 +634,60 @@ def validate(compose_file: Path = typer.Option(config.COMPOSE_FILE)):
|
|
|
466
634
|
typer.echo("compose file is valid.")
|
|
467
635
|
|
|
468
636
|
|
|
469
|
-
@
|
|
470
|
-
def
|
|
471
|
-
|
|
637
|
+
@system_app.command("diagnose")
|
|
638
|
+
def system_diagnose(
|
|
639
|
+
lines: int = typer.Option(
|
|
640
|
+
100, "--lines", "-n", help="Number of log lines to analyze"
|
|
641
|
+
),
|
|
642
|
+
all_containers: bool = typer.Option(
|
|
643
|
+
False, "--all", help="Check all containers, not only problematic ones"
|
|
644
|
+
),
|
|
645
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
646
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
|
647
|
+
):
|
|
648
|
+
"""Diagnose VPN containers and report health."""
|
|
472
649
|
|
|
473
|
-
from .docker_ops import
|
|
650
|
+
from .docker_ops import (
|
|
651
|
+
get_problematic_containers,
|
|
652
|
+
get_vpn_containers,
|
|
653
|
+
get_container_diagnostics,
|
|
654
|
+
analyze_container_logs,
|
|
655
|
+
)
|
|
656
|
+
from .diagnostics import DiagnosticAnalyzer
|
|
474
657
|
|
|
475
|
-
|
|
476
|
-
|
|
658
|
+
analyzer = DiagnosticAnalyzer()
|
|
659
|
+
containers = (
|
|
660
|
+
get_vpn_containers(all=True)
|
|
661
|
+
if all_containers
|
|
662
|
+
else get_problematic_containers(all=True)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
summary: list[dict[str, object]] = []
|
|
666
|
+
for container in containers:
|
|
667
|
+
diag = get_container_diagnostics(container)
|
|
668
|
+
results = analyze_container_logs(container.name, lines=lines, analyzer=analyzer)
|
|
669
|
+
score = analyzer.health_score(results)
|
|
670
|
+
entry = {
|
|
671
|
+
"container": container.name,
|
|
672
|
+
"status": diag["status"],
|
|
673
|
+
"health": score,
|
|
674
|
+
"issues": [r.message for r in results],
|
|
675
|
+
"recommendations": [r.recommendation for r in results],
|
|
676
|
+
}
|
|
677
|
+
summary.append(entry)
|
|
678
|
+
|
|
679
|
+
if json_output:
|
|
680
|
+
typer.echo(json.dumps(summary, indent=2))
|
|
477
681
|
else:
|
|
478
|
-
|
|
479
|
-
|
|
682
|
+
if not summary:
|
|
683
|
+
typer.echo("No containers to diagnose.")
|
|
684
|
+
for entry in summary:
|
|
685
|
+
typer.echo(
|
|
686
|
+
f"{entry['container']}: status={entry['status']} health={entry['health']}"
|
|
687
|
+
)
|
|
688
|
+
if verbose or entry["issues"]:
|
|
689
|
+
for issue, rec in zip(entry["issues"], entry["recommendations"]):
|
|
690
|
+
typer.echo(f" - {issue}: {rec}")
|
|
480
691
|
|
|
481
692
|
|
|
482
693
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Diagnostic analysis for proxy2vpn containers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import re
|
|
7
|
+
from typing import Iterable, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DiagnosticResult:
|
|
12
|
+
"""Result of analyzing a log line."""
|
|
13
|
+
|
|
14
|
+
pattern: str
|
|
15
|
+
severity: str
|
|
16
|
+
message: str
|
|
17
|
+
recommendation: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DiagnosticAnalyzer:
|
|
21
|
+
"""Analyze container logs for common error patterns."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.patterns: list[tuple[re.Pattern[str], str, str, str]] = [
|
|
25
|
+
(
|
|
26
|
+
re.compile(r"authentication (failed|failure)", re.I),
|
|
27
|
+
"critical",
|
|
28
|
+
"Authentication failure",
|
|
29
|
+
"Check credentials and provider configuration.",
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
re.compile(r"certificate|ssl", re.I),
|
|
33
|
+
"critical",
|
|
34
|
+
"Certificate or SSL issue",
|
|
35
|
+
"Verify certificates and TLS settings.",
|
|
36
|
+
),
|
|
37
|
+
(
|
|
38
|
+
re.compile(
|
|
39
|
+
r"(connection (refused|timed out|unreachable))|(network is unreachable)",
|
|
40
|
+
re.I,
|
|
41
|
+
),
|
|
42
|
+
"critical",
|
|
43
|
+
"Network connectivity issue",
|
|
44
|
+
"Ensure network access and proxy settings.",
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
re.compile(r"rate limit", re.I),
|
|
48
|
+
"warning",
|
|
49
|
+
"Rate limiting detected",
|
|
50
|
+
"Reduce request rate or check provider limits.",
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
re.compile(r"(dns (resolution|lookup) failed)|no such host", re.I),
|
|
54
|
+
"critical",
|
|
55
|
+
"DNS resolution failure",
|
|
56
|
+
"Check DNS configuration or server availability.",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
re.compile(r"openvpn", re.I),
|
|
60
|
+
"info",
|
|
61
|
+
"OpenVPN error",
|
|
62
|
+
"Review OpenVPN configuration for issues.",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
re.compile(r"wireguard", re.I),
|
|
66
|
+
"info",
|
|
67
|
+
"WireGuard error",
|
|
68
|
+
"Review WireGuard configuration for issues.",
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
def analyze(self, log_lines: Iterable[str]) -> List[DiagnosticResult]:
|
|
73
|
+
"""Return diagnostic results for matching log lines."""
|
|
74
|
+
|
|
75
|
+
results: List[DiagnosticResult] = []
|
|
76
|
+
for line in log_lines:
|
|
77
|
+
for pattern, severity, message, recommendation in self.patterns:
|
|
78
|
+
if pattern.search(line):
|
|
79
|
+
results.append(
|
|
80
|
+
DiagnosticResult(
|
|
81
|
+
pattern=pattern.pattern,
|
|
82
|
+
severity=severity,
|
|
83
|
+
message=message,
|
|
84
|
+
recommendation=recommendation,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
def health_score(self, results: Iterable[DiagnosticResult]) -> int:
|
|
90
|
+
"""Return a simple health score based on diagnostic results."""
|
|
91
|
+
|
|
92
|
+
score = 100
|
|
93
|
+
for res in results:
|
|
94
|
+
if res.severity == "critical":
|
|
95
|
+
score -= 40
|
|
96
|
+
elif res.severity == "warning":
|
|
97
|
+
score -= 20
|
|
98
|
+
else:
|
|
99
|
+
score -= 10
|
|
100
|
+
return max(score, 0)
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Iterable, Iterator
|
|
6
6
|
|
|
7
|
+
from .diagnostics import DiagnosticAnalyzer, DiagnosticResult
|
|
8
|
+
|
|
7
9
|
import docker
|
|
8
10
|
import requests
|
|
9
11
|
from docker.models.containers import Container
|
|
@@ -89,6 +91,49 @@ def get_vpn_containers(all: bool = False) -> list[Container]:
|
|
|
89
91
|
return client.containers.list(all=all, filters={"label": "vpn.type=vpn"})
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
def get_problematic_containers(all: bool = False) -> list[Container]:
|
|
95
|
+
"""Return containers that are not running properly."""
|
|
96
|
+
|
|
97
|
+
containers = get_vpn_containers(all=all)
|
|
98
|
+
problematic: list[Container] = []
|
|
99
|
+
for container in containers:
|
|
100
|
+
container.reload()
|
|
101
|
+
state = container.attrs.get("State", {})
|
|
102
|
+
if (
|
|
103
|
+
container.status != "running"
|
|
104
|
+
or state.get("ExitCode", 0) != 0
|
|
105
|
+
or state.get("RestartCount", 0) > 0
|
|
106
|
+
):
|
|
107
|
+
problematic.append(container)
|
|
108
|
+
return problematic
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_container_diagnostics(container: Container) -> dict:
|
|
112
|
+
"""Return diagnostic information for a container."""
|
|
113
|
+
|
|
114
|
+
container.reload()
|
|
115
|
+
state = container.attrs.get("State", {})
|
|
116
|
+
return {
|
|
117
|
+
"name": container.name,
|
|
118
|
+
"status": container.status,
|
|
119
|
+
"exit_code": state.get("ExitCode"),
|
|
120
|
+
"restart_count": state.get("RestartCount", 0),
|
|
121
|
+
"started_at": state.get("StartedAt"),
|
|
122
|
+
"finished_at": state.get("FinishedAt"),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def analyze_container_logs(
|
|
127
|
+
name: str, lines: int = 100, analyzer: DiagnosticAnalyzer | None = None
|
|
128
|
+
) -> list[DiagnosticResult]:
|
|
129
|
+
"""Analyze container logs and return diagnostic results."""
|
|
130
|
+
|
|
131
|
+
if analyzer is None:
|
|
132
|
+
analyzer = DiagnosticAnalyzer()
|
|
133
|
+
logs = list(container_logs(name, lines=lines, follow=False))
|
|
134
|
+
return analyzer.analyze(logs)
|
|
135
|
+
|
|
136
|
+
|
|
92
137
|
def start_all_vpn_containers() -> list[tuple[str, bool]]:
|
|
93
138
|
"""Start all VPN containers.
|
|
94
139
|
|
|
@@ -329,11 +329,11 @@ class HelpfulTyper(typer.Typer):
|
|
|
329
329
|
f"{command_path} list-providers",
|
|
330
330
|
f"{command_path} list-countries nordvpn",
|
|
331
331
|
]
|
|
332
|
-
elif "
|
|
332
|
+
elif "system" in command_path:
|
|
333
333
|
examples = [
|
|
334
|
-
f"{command_path}
|
|
335
|
-
f"{command_path}
|
|
336
|
-
f"{command_path}
|
|
334
|
+
f"{command_path} init",
|
|
335
|
+
f"{command_path} validate",
|
|
336
|
+
f"{command_path} diagnose",
|
|
337
337
|
]
|
|
338
338
|
elif "preset" in command_path:
|
|
339
339
|
examples = [
|
|
@@ -355,7 +355,7 @@ class HelpfulTyper(typer.Typer):
|
|
|
355
355
|
"vpn": f"{base_url}#vpn-management",
|
|
356
356
|
"profile": f"{base_url}#profile-management",
|
|
357
357
|
"servers": f"{base_url}#server-lists",
|
|
358
|
-
"
|
|
358
|
+
"system": f"{base_url}#system-operations",
|
|
359
359
|
"preset": f"{base_url}#presets",
|
|
360
360
|
}
|
|
361
361
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy2vpn
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Proxy2VPN Python utilities
|
|
5
5
|
Author-email: Serhii Khalymon <serhii.khalymon@pm.me>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Python command-line interface for managing multiple VPN containers with Docker.
|
|
|
25
25
|
## Features
|
|
26
26
|
- Manage VPN credentials as reusable profiles
|
|
27
27
|
- Create and control VPN services
|
|
28
|
-
-
|
|
28
|
+
- Multi-service control with `--all` flags
|
|
29
29
|
- Query and validate provider server locations
|
|
30
30
|
- Apply predefined presets for common setups
|
|
31
31
|
|
|
@@ -61,7 +61,7 @@ proxy2vpn --help
|
|
|
61
61
|
## Quick Start
|
|
62
62
|
1. Initialize the compose file:
|
|
63
63
|
```bash
|
|
64
|
-
proxy2vpn init
|
|
64
|
+
proxy2vpn system init
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
2. Create a profile file with your VPN credentials:
|
|
@@ -87,13 +87,15 @@ proxy2vpn --help
|
|
|
87
87
|
5. View status and test connectivity:
|
|
88
88
|
```bash
|
|
89
89
|
proxy2vpn vpn list
|
|
90
|
-
proxy2vpn test vpn1
|
|
90
|
+
proxy2vpn vpn test vpn1
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
## Command overview
|
|
94
94
|
|
|
95
|
-
###
|
|
96
|
-
- `proxy2vpn init [--force]`
|
|
95
|
+
### System operations
|
|
96
|
+
- `proxy2vpn system init [--force]`
|
|
97
|
+
- `proxy2vpn system validate`
|
|
98
|
+
- `proxy2vpn system diagnose [--lines N] [--all] [--verbose] [--json]`
|
|
97
99
|
|
|
98
100
|
### Profiles
|
|
99
101
|
- `proxy2vpn profile create NAME ENV_FILE`
|
|
@@ -102,18 +104,13 @@ proxy2vpn --help
|
|
|
102
104
|
|
|
103
105
|
### VPN services
|
|
104
106
|
- `proxy2vpn vpn create NAME PROFILE [--port PORT] [--provider PROVIDER] [--location LOCATION]`
|
|
105
|
-
- `proxy2vpn vpn list`
|
|
106
|
-
- `proxy2vpn vpn start NAME`
|
|
107
|
-
- `proxy2vpn vpn stop NAME`
|
|
108
|
-
- `proxy2vpn vpn restart NAME`
|
|
107
|
+
- `proxy2vpn vpn list [--diagnose] [--ips-only]`
|
|
108
|
+
- `proxy2vpn vpn start [NAME | --all]`
|
|
109
|
+
- `proxy2vpn vpn stop [NAME | --all]`
|
|
110
|
+
- `proxy2vpn vpn restart [NAME | --all]`
|
|
109
111
|
- `proxy2vpn vpn logs NAME [--lines N] [--follow]`
|
|
110
|
-
- `proxy2vpn vpn delete NAME`
|
|
111
|
-
|
|
112
|
-
### Bulk operations
|
|
113
|
-
- `proxy2vpn bulk up`
|
|
114
|
-
- `proxy2vpn bulk down`
|
|
115
|
-
- `proxy2vpn bulk status`
|
|
116
|
-
- `proxy2vpn bulk ips`
|
|
112
|
+
- `proxy2vpn vpn delete [NAME | --all]`
|
|
113
|
+
- `proxy2vpn vpn test NAME`
|
|
117
114
|
|
|
118
115
|
### Server database
|
|
119
116
|
- `proxy2vpn servers update`
|
|
@@ -126,9 +123,6 @@ proxy2vpn --help
|
|
|
126
123
|
- `proxy2vpn preset list`
|
|
127
124
|
- `proxy2vpn preset apply PRESET SERVICE [--port PORT]`
|
|
128
125
|
|
|
129
|
-
### Testing
|
|
130
|
-
- `proxy2vpn test SERVICE` – verify that a proxy container is reachable
|
|
131
|
-
|
|
132
126
|
## Development
|
|
133
127
|
|
|
134
128
|
### Setup
|
|
@@ -8,6 +8,7 @@ src/proxy2vpn/compose_manager.py
|
|
|
8
8
|
src/proxy2vpn/compose_utils.py
|
|
9
9
|
src/proxy2vpn/compose_validator.py
|
|
10
10
|
src/proxy2vpn/config.py
|
|
11
|
+
src/proxy2vpn/diagnostics.py
|
|
11
12
|
src/proxy2vpn/docker_ops.py
|
|
12
13
|
src/proxy2vpn/models.py
|
|
13
14
|
src/proxy2vpn/preset_manager.py
|
|
@@ -26,4 +27,5 @@ tests/test_compose_validator.py
|
|
|
26
27
|
tests/test_docker_ops.py
|
|
27
28
|
tests/test_init_command.py
|
|
28
29
|
tests/test_preset_manager.py
|
|
29
|
-
tests/test_server_manager.py
|
|
30
|
+
tests/test_server_manager.py
|
|
31
|
+
tests/test_version_option.py
|
|
@@ -20,7 +20,7 @@ def _run_proxy2vpn(args, cwd):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def test_init_creates_compose(tmp_path):
|
|
23
|
-
result = _run_proxy2vpn(["init"], tmp_path)
|
|
23
|
+
result = _run_proxy2vpn(["system", "init"], tmp_path)
|
|
24
24
|
assert result.returncode == 0
|
|
25
25
|
compose = tmp_path / "compose.yml"
|
|
26
26
|
assert compose.exists()
|
|
@@ -34,8 +34,8 @@ def test_init_requires_force(tmp_path):
|
|
|
34
34
|
compose = tmp_path / "compose.yml"
|
|
35
35
|
compose.write_text("services: {}\n")
|
|
36
36
|
|
|
37
|
-
result = _run_proxy2vpn(["init"], tmp_path)
|
|
37
|
+
result = _run_proxy2vpn(["system", "init"], tmp_path)
|
|
38
38
|
assert result.returncode != 0
|
|
39
39
|
|
|
40
|
-
result = _run_proxy2vpn(["init", "--force"], tmp_path)
|
|
40
|
+
result = _run_proxy2vpn(["system", "init", "--force"], tmp_path)
|
|
41
41
|
assert result.returncode == 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
repo_root = pathlib.Path(__file__).resolve().parents[1]
|
|
7
|
+
sys.path.insert(0, str(repo_root / "src"))
|
|
8
|
+
import proxy2vpn # noqa
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
|
12
|
+
env = os.environ.copy()
|
|
13
|
+
env["PYTHONPATH"] = str(repo_root / "src")
|
|
14
|
+
return subprocess.run(
|
|
15
|
+
[sys.executable, "-m", "proxy2vpn", *args],
|
|
16
|
+
capture_output=True,
|
|
17
|
+
text=True,
|
|
18
|
+
env=env,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_version_flag_long():
|
|
23
|
+
result = _run_cli("--version")
|
|
24
|
+
assert result.returncode == 0
|
|
25
|
+
assert result.stdout.strip() == proxy2vpn.__version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_version_flag_short():
|
|
29
|
+
result = _run_cli("-V")
|
|
30
|
+
assert result.returncode == 0
|
|
31
|
+
assert result.stdout.strip() == proxy2vpn.__version__
|
|
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
|