wayscloud-cli 0.2.3__tar.gz → 0.4.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.
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/PKG-INFO +2 -4
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/pyproject.toml +2 -2
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/setup.cfg +1 -1
- wayscloud_cli-0.4.0/wayscloud_cli/__init__.py +1 -0
- wayscloud_cli-0.4.0/wayscloud_cli/commands/app.py +295 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/db.py +74 -11
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/dns.py +89 -0
- wayscloud_cli-0.4.0/wayscloud_cli/commands/iot.py +558 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/redis.py +104 -9
- wayscloud_cli-0.4.0/wayscloud_cli/commands/storage.py +182 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/vps.py +124 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/PKG-INFO +2 -4
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/SOURCES.txt +0 -1
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/requires.txt +1 -1
- wayscloud_cli-0.2.3/LICENSE +0 -21
- wayscloud_cli-0.2.3/wayscloud_cli/__init__.py +0 -1
- wayscloud_cli-0.2.3/wayscloud_cli/commands/app.py +0 -136
- wayscloud_cli-0.2.3/wayscloud_cli/commands/iot.py +0 -165
- wayscloud_cli-0.2.3/wayscloud_cli/commands/storage.py +0 -87
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/README.md +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/tests/test_smoke.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/__main__.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/__init__.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/login.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/commands/shell.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/config.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/output.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli/sdk.py +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/dependency_links.txt +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/entry_points.txt +0 -0
- {wayscloud_cli-0.2.3 → wayscloud_cli-0.4.0}/wayscloud_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wayscloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://wayscloud.services
|
|
@@ -19,12 +19,10 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Operating System :: OS Independent
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
|
-
|
|
23
|
-
Requires-Dist: wayscloud>=0.1.0
|
|
22
|
+
Requires-Dist: wayscloud>=0.3.0
|
|
24
23
|
Requires-Dist: typer>=0.9
|
|
25
24
|
Requires-Dist: rich>=13.0
|
|
26
25
|
Requires-Dist: websockets>=12.0
|
|
27
|
-
Dynamic: license-file
|
|
28
26
|
|
|
29
27
|
# WAYSCloud CLI
|
|
30
28
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wayscloud-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Operating System :: OS Independent",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"wayscloud>=0.
|
|
25
|
+
"wayscloud>=0.3.0",
|
|
26
26
|
"typer>=0.9",
|
|
27
27
|
"rich>=13.0",
|
|
28
28
|
"websockets>=12.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloud app commands — App Platform provisioning and control.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
from ..sdk import get_client, sdk_call
|
|
9
|
+
from ..output import print_table, print_object, print_json, is_json_mode
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="App platform")
|
|
12
|
+
|
|
13
|
+
# ── Env sub-group ────────────────────────────────────────────────
|
|
14
|
+
env_app = typer.Typer(help="Environment variables")
|
|
15
|
+
app.add_typer(env_app, name="env")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@env_app.command("list")
|
|
19
|
+
def env_list(
|
|
20
|
+
app_id: str = typer.Argument(..., help="App ID or name"),
|
|
21
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
22
|
+
):
|
|
23
|
+
"""List environment variable keys for an app."""
|
|
24
|
+
c = get_client(token)
|
|
25
|
+
env = sdk_call(c.apps.env_vars, app_id)
|
|
26
|
+
|
|
27
|
+
if is_json_mode():
|
|
28
|
+
print_json(env)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if not env:
|
|
32
|
+
print("No environment variables set.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
print(f"Environment variables ({len(env)}):\n")
|
|
36
|
+
for key in sorted(env.keys()):
|
|
37
|
+
print(f" {key}=***")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@env_app.command("set")
|
|
41
|
+
def env_set(
|
|
42
|
+
app_id: str = typer.Argument(..., help="App ID or name"),
|
|
43
|
+
pairs: List[str] = typer.Argument(..., help="KEY=value pairs"),
|
|
44
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
45
|
+
):
|
|
46
|
+
"""Set one or more environment variables."""
|
|
47
|
+
c = get_client(token)
|
|
48
|
+
|
|
49
|
+
for pair in pairs:
|
|
50
|
+
if "=" not in pair:
|
|
51
|
+
print(f"Error: Invalid format '{pair}'. Use KEY=value", err=True)
|
|
52
|
+
raise typer.Exit(code=1)
|
|
53
|
+
key, _, value = pair.partition("=")
|
|
54
|
+
sdk_call(c.apps.set_env, app_id, key.strip(), value)
|
|
55
|
+
print(f" Set: {key.strip()}")
|
|
56
|
+
|
|
57
|
+
print(f"\n{len(pairs)} variable(s) updated.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@env_app.command("unset")
|
|
61
|
+
def env_unset(
|
|
62
|
+
app_id: str = typer.Argument(..., help="App ID or name"),
|
|
63
|
+
keys: List[str] = typer.Argument(..., help="Variable names to remove"),
|
|
64
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
65
|
+
):
|
|
66
|
+
"""Remove one or more environment variables."""
|
|
67
|
+
c = get_client(token)
|
|
68
|
+
|
|
69
|
+
for key in keys:
|
|
70
|
+
sdk_call(c.apps.unset_env, app_id, key)
|
|
71
|
+
print(f" Removed: {key}")
|
|
72
|
+
|
|
73
|
+
print(f"\n{len(keys)} variable(s) removed.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Core commands ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
@app.command("list")
|
|
79
|
+
def list_apps(
|
|
80
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
81
|
+
):
|
|
82
|
+
"""List your apps."""
|
|
83
|
+
c = get_client(token)
|
|
84
|
+
data = sdk_call(c.apps.list)
|
|
85
|
+
apps_list = data if isinstance(data, list) else data.get("apps", []) if isinstance(data, dict) else []
|
|
86
|
+
|
|
87
|
+
if is_json_mode():
|
|
88
|
+
print_json(apps_list)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if not apps_list:
|
|
92
|
+
print("No apps found.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
rows = [{"id": str(a.get("id", ""))[:14], "name": a.get("name", ""),
|
|
96
|
+
"status": a.get("status", ""), "plan": a.get("plan", a.get("plan_code", "")),
|
|
97
|
+
"url": a.get("default_url", ""), "created_at": a.get("created_at", "")}
|
|
98
|
+
for a in apps_list]
|
|
99
|
+
|
|
100
|
+
print_table(rows, [
|
|
101
|
+
("id", "ID", 14), ("name", "Name", 20), ("status", "Status", 10),
|
|
102
|
+
("plan", "Plan", 16), ("url", "URL", 30), ("created_at", "Created", 20),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("create")
|
|
107
|
+
def create_app(
|
|
108
|
+
name: str = typer.Argument(..., help="App name"),
|
|
109
|
+
plan: str = typer.Option("app-basic", "--plan", help="Plan code"),
|
|
110
|
+
region: str = typer.Option("no", "--region", help="Region"),
|
|
111
|
+
port: int = typer.Option(8080, "--port", help="Application port"),
|
|
112
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
113
|
+
):
|
|
114
|
+
"""Create a new app."""
|
|
115
|
+
c = get_client(token)
|
|
116
|
+
data = sdk_call(c.apps.create, name, plan=plan, region=region, port=port)
|
|
117
|
+
if is_json_mode():
|
|
118
|
+
print_json(data)
|
|
119
|
+
else:
|
|
120
|
+
print(f"App created: {data.get('name', name)}")
|
|
121
|
+
print_object(data, [("id", "ID"), ("name", "Name"), ("status", "Status"),
|
|
122
|
+
("plan", "Plan"), ("default_url", "URL")])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("info")
|
|
126
|
+
def info_app(
|
|
127
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
128
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
129
|
+
):
|
|
130
|
+
"""Show app details."""
|
|
131
|
+
c = get_client(token)
|
|
132
|
+
data = sdk_call(c.apps.get, app_id)
|
|
133
|
+
print_object(data, [
|
|
134
|
+
("id", "ID"), ("name", "Name"), ("status", "Status"), ("plan", "Plan"),
|
|
135
|
+
("region", "Region"), ("port", "Port"), ("default_url", "URL"),
|
|
136
|
+
("image_uri", "Image"), ("created_at", "Created"),
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("delete")
|
|
141
|
+
def delete_app(
|
|
142
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
143
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
144
|
+
confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
|
|
145
|
+
):
|
|
146
|
+
"""Delete an app (permanent)."""
|
|
147
|
+
if not confirm:
|
|
148
|
+
print(f"This will permanently delete app {app_id} and all its data.")
|
|
149
|
+
print("Use --confirm to proceed.")
|
|
150
|
+
raise typer.Exit(code=1)
|
|
151
|
+
|
|
152
|
+
c = get_client(token)
|
|
153
|
+
data = sdk_call(c.apps.delete, app_id)
|
|
154
|
+
if is_json_mode():
|
|
155
|
+
print_json(data)
|
|
156
|
+
else:
|
|
157
|
+
print(f"App {app_id}: deleted")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command("deploy")
|
|
161
|
+
def deploy_app(
|
|
162
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
163
|
+
image: str = typer.Option(..., "--image", help="Container image URI"),
|
|
164
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
165
|
+
):
|
|
166
|
+
"""Deploy an app from a container image."""
|
|
167
|
+
c = get_client(token)
|
|
168
|
+
data = sdk_call(c.apps.deploy, app_id, image)
|
|
169
|
+
if is_json_mode():
|
|
170
|
+
print_json(data)
|
|
171
|
+
else:
|
|
172
|
+
print(f"App {app_id}: deploying image {image}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("start")
|
|
176
|
+
def start_app(
|
|
177
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
178
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
179
|
+
):
|
|
180
|
+
"""Start an app."""
|
|
181
|
+
c = get_client(token)
|
|
182
|
+
data = sdk_call(c.apps.start, app_id)
|
|
183
|
+
if is_json_mode():
|
|
184
|
+
print_json(data)
|
|
185
|
+
else:
|
|
186
|
+
print(f"App {app_id}: {data.get('message', 'starting')}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command("stop")
|
|
190
|
+
def stop_app(
|
|
191
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
192
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
193
|
+
):
|
|
194
|
+
"""Stop an app."""
|
|
195
|
+
c = get_client(token)
|
|
196
|
+
data = sdk_call(c.apps.stop, app_id)
|
|
197
|
+
if is_json_mode():
|
|
198
|
+
print_json(data)
|
|
199
|
+
else:
|
|
200
|
+
print(f"App {app_id}: {data.get('message', 'stopping')}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command("restart")
|
|
204
|
+
def restart_app(
|
|
205
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
206
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
207
|
+
):
|
|
208
|
+
"""Restart an app."""
|
|
209
|
+
c = get_client(token)
|
|
210
|
+
data = sdk_call(c.apps.restart, app_id)
|
|
211
|
+
if is_json_mode():
|
|
212
|
+
print_json(data)
|
|
213
|
+
else:
|
|
214
|
+
print(f"App {app_id}: {data.get('message', 'restarting')}")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@app.command("logs")
|
|
218
|
+
def logs_app(
|
|
219
|
+
app_id: str = typer.Argument(..., help="App ID"),
|
|
220
|
+
lines: int = typer.Option(100, "--lines", "-n", help="Number of lines"),
|
|
221
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
222
|
+
):
|
|
223
|
+
"""Show app logs."""
|
|
224
|
+
c = get_client(token)
|
|
225
|
+
data = sdk_call(c.apps.logs, app_id, lines=lines)
|
|
226
|
+
|
|
227
|
+
if is_json_mode():
|
|
228
|
+
print_json(data)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
log_lines = data if isinstance(data, list) else []
|
|
232
|
+
if not log_lines:
|
|
233
|
+
print("No logs available.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
for line in log_lines:
|
|
237
|
+
if isinstance(line, dict):
|
|
238
|
+
ts = line.get("timestamp", "")
|
|
239
|
+
msg = line.get("message", line.get("log", str(line)))
|
|
240
|
+
print(f"{ts} {msg}")
|
|
241
|
+
else:
|
|
242
|
+
print(line)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.command("plans")
|
|
246
|
+
def list_plans(
|
|
247
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
248
|
+
):
|
|
249
|
+
"""List available app plans."""
|
|
250
|
+
c = get_client(token)
|
|
251
|
+
data = sdk_call(c.apps.plans)
|
|
252
|
+
plans_list = data if isinstance(data, list) else []
|
|
253
|
+
|
|
254
|
+
if is_json_mode():
|
|
255
|
+
print_json(plans_list)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if not plans_list:
|
|
259
|
+
print("No plans available.")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
rows = [{"code": p.get("code", p.get("plan_code", "")),
|
|
263
|
+
"name": p.get("name", ""),
|
|
264
|
+
"cpu": str(p.get("cpu", "")),
|
|
265
|
+
"memory": str(p.get("memory_mb", p.get("memory", ""))),
|
|
266
|
+
"price": str(p.get("price_monthly", p.get("price", "")))}
|
|
267
|
+
for p in plans_list]
|
|
268
|
+
|
|
269
|
+
print_table(rows, [
|
|
270
|
+
("code", "Code", 20), ("name", "Name", 20), ("cpu", "CPU", 6),
|
|
271
|
+
("memory", "Memory MB", 10), ("price", "Price/mo", 10),
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command("regions")
|
|
276
|
+
def list_regions(
|
|
277
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
278
|
+
):
|
|
279
|
+
"""List available app regions."""
|
|
280
|
+
c = get_client(token)
|
|
281
|
+
data = sdk_call(c.apps.regions)
|
|
282
|
+
|
|
283
|
+
if is_json_mode():
|
|
284
|
+
print_json(data)
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
regions_list = data if isinstance(data, list) else []
|
|
288
|
+
if not regions_list:
|
|
289
|
+
print("No regions available.")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
for r in regions_list:
|
|
293
|
+
code = r.get("code", r.get("region", ""))
|
|
294
|
+
name = r.get("name", r.get("label", ""))
|
|
295
|
+
print(f" {code:<10} {name}")
|
|
@@ -65,20 +65,10 @@ def database_info(
|
|
|
65
65
|
c = get_client(token)
|
|
66
66
|
data = sdk_call(c.database.get, db_type, name)
|
|
67
67
|
|
|
68
|
-
# Merge credentials if available
|
|
69
|
-
try:
|
|
70
|
-
creds = c.database.credentials(db_type, name)
|
|
71
|
-
if isinstance(creds, dict):
|
|
72
|
-
data.update(creds)
|
|
73
|
-
except Exception as e:
|
|
74
|
-
import sys
|
|
75
|
-
print(f"Warning: could not fetch credentials: {e}", file=sys.stderr)
|
|
76
|
-
|
|
77
68
|
print_object(data, [
|
|
78
69
|
("name", "Name"), ("type", "Type"), ("tier", "Tier"), ("status", "Status"),
|
|
79
70
|
("size_mb", "Size (MB)"), ("host", "Host"), ("port", "Port"),
|
|
80
|
-
("
|
|
81
|
-
("connection_string", "Connection String"), ("created_at", "Created"),
|
|
71
|
+
("created_at", "Created"),
|
|
82
72
|
])
|
|
83
73
|
|
|
84
74
|
|
|
@@ -101,3 +91,76 @@ def delete_database(
|
|
|
101
91
|
print_json(data)
|
|
102
92
|
else:
|
|
103
93
|
print(f"Database {name}: deleted")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("tiers")
|
|
97
|
+
def list_tiers(
|
|
98
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
99
|
+
):
|
|
100
|
+
"""List available database tiers."""
|
|
101
|
+
c = get_client(token)
|
|
102
|
+
data = sdk_call(c.database.tiers)
|
|
103
|
+
if is_json_mode():
|
|
104
|
+
print_json(data)
|
|
105
|
+
return
|
|
106
|
+
tiers = data if isinstance(data, list) else []
|
|
107
|
+
for t in tiers:
|
|
108
|
+
name = t.get("name", t.get("tier", ""))
|
|
109
|
+
desc = t.get("description", "")
|
|
110
|
+
print(f" {name:<16} {desc}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("firewall")
|
|
114
|
+
def list_firewall(
|
|
115
|
+
db_type: str = typer.Argument(..., help="Database type (postgresql/mariadb)"),
|
|
116
|
+
name: str = typer.Argument(..., help="Database name"),
|
|
117
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
118
|
+
):
|
|
119
|
+
"""List firewall rules for a database."""
|
|
120
|
+
c = get_client(token)
|
|
121
|
+
data = sdk_call(c.database.firewall_rules, db_type, name)
|
|
122
|
+
rules = data if isinstance(data, list) else []
|
|
123
|
+
if is_json_mode():
|
|
124
|
+
print_json(rules)
|
|
125
|
+
return
|
|
126
|
+
if not rules:
|
|
127
|
+
print("No firewall rules.")
|
|
128
|
+
return
|
|
129
|
+
rows = [{"id": str(r.get("id", ""))[:12], "ip": r.get("ip_address", ""),
|
|
130
|
+
"desc": r.get("description", ""), "created": r.get("created_at", "")[:19]}
|
|
131
|
+
for r in rules]
|
|
132
|
+
print_table(rows, [("id", "ID", 14), ("ip", "IP Address", 20),
|
|
133
|
+
("desc", "Description", 24), ("created", "Created", 20)])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command("firewall-add")
|
|
137
|
+
def add_firewall(
|
|
138
|
+
db_type: str = typer.Argument(..., help="Database type (postgresql/mariadb)"),
|
|
139
|
+
name: str = typer.Argument(..., help="Database name"),
|
|
140
|
+
ip: str = typer.Option(..., "--ip", help="IP address to whitelist"),
|
|
141
|
+
description: str = typer.Option("", "--description", help="Description"),
|
|
142
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
143
|
+
):
|
|
144
|
+
"""Add a firewall rule to whitelist an IP address."""
|
|
145
|
+
c = get_client(token)
|
|
146
|
+
data = sdk_call(c.database.add_firewall_rule, db_type, name, ip_address=ip, description=description)
|
|
147
|
+
if is_json_mode():
|
|
148
|
+
print_json(data)
|
|
149
|
+
else:
|
|
150
|
+
print(f"Firewall rule added: {ip}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("firewall-remove")
|
|
154
|
+
def remove_firewall(
|
|
155
|
+
db_type: str = typer.Argument(..., help="Database type (postgresql/mariadb)"),
|
|
156
|
+
name: str = typer.Argument(..., help="Database name"),
|
|
157
|
+
rule_id: str = typer.Option(..., "--rule-id", help="Rule ID"),
|
|
158
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
159
|
+
):
|
|
160
|
+
"""Remove a firewall rule."""
|
|
161
|
+
c = get_client(token)
|
|
162
|
+
data = sdk_call(c.database.remove_firewall_rule, db_type, name, rule_id)
|
|
163
|
+
if is_json_mode():
|
|
164
|
+
print_json(data)
|
|
165
|
+
else:
|
|
166
|
+
print(f"Firewall rule {rule_id}: removed")
|
|
@@ -38,6 +38,25 @@ def list_zones(
|
|
|
38
38
|
])
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
@app.command("zones-info")
|
|
42
|
+
def zone_info(
|
|
43
|
+
zone_name: str = typer.Argument(..., help="Zone name"),
|
|
44
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
45
|
+
):
|
|
46
|
+
"""Show details for a DNS zone."""
|
|
47
|
+
c = get_client(token)
|
|
48
|
+
data = sdk_call(c.dns.get, zone_name)
|
|
49
|
+
if is_json_mode():
|
|
50
|
+
print_json(data)
|
|
51
|
+
else:
|
|
52
|
+
from ..output import print_object
|
|
53
|
+
print_object(data, [
|
|
54
|
+
("name", "Name"), ("status", "Status"), ("zone_type", "Type"),
|
|
55
|
+
("record_count", "Records"), ("dnssec_enabled", "DNSSEC"),
|
|
56
|
+
("created_at", "Created"),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
|
|
41
60
|
@app.command("zones-create")
|
|
42
61
|
def create_zone(
|
|
43
62
|
name: str = typer.Argument(..., help="Zone name"),
|
|
@@ -169,3 +188,73 @@ def delete_record(
|
|
|
169
188
|
print_json(data)
|
|
170
189
|
else:
|
|
171
190
|
print(f"Record {record_id}: deleted")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command("dnssec-status")
|
|
194
|
+
def dnssec_status(
|
|
195
|
+
zone_name: str = typer.Argument(..., help="Zone name"),
|
|
196
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
197
|
+
):
|
|
198
|
+
"""Show DNSSEC status for a zone."""
|
|
199
|
+
c = get_client(token)
|
|
200
|
+
data = sdk_call(c.dns.dnssec_status, zone_name)
|
|
201
|
+
if is_json_mode():
|
|
202
|
+
print_json(data)
|
|
203
|
+
else:
|
|
204
|
+
from ..output import print_object
|
|
205
|
+
print_object(data, [
|
|
206
|
+
("enabled", "Enabled"), ("ds_records", "DS Records"),
|
|
207
|
+
("algorithm", "Algorithm"), ("key_tag", "Key Tag"),
|
|
208
|
+
])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command("dnssec-activate")
|
|
212
|
+
def dnssec_activate(
|
|
213
|
+
zone_name: str = typer.Argument(..., help="Zone name"),
|
|
214
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
215
|
+
):
|
|
216
|
+
"""Activate DNSSEC for a zone."""
|
|
217
|
+
c = get_client(token)
|
|
218
|
+
data = sdk_call(c.dns.dnssec_activate, zone_name)
|
|
219
|
+
if is_json_mode():
|
|
220
|
+
print_json(data)
|
|
221
|
+
else:
|
|
222
|
+
print(f"DNSSEC activated for {zone_name}.")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command("dnssec-deactivate")
|
|
226
|
+
def dnssec_deactivate(
|
|
227
|
+
zone_name: str = typer.Argument(..., help="Zone name"),
|
|
228
|
+
confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
|
|
229
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
230
|
+
):
|
|
231
|
+
"""Deactivate DNSSEC for a zone."""
|
|
232
|
+
if not confirm:
|
|
233
|
+
print(f"This will deactivate DNSSEC for {zone_name}.")
|
|
234
|
+
print("Use --confirm to proceed.")
|
|
235
|
+
raise typer.Exit(code=1)
|
|
236
|
+
|
|
237
|
+
c = get_client(token)
|
|
238
|
+
data = sdk_call(c.dns.dnssec_deactivate, zone_name)
|
|
239
|
+
if is_json_mode():
|
|
240
|
+
print_json(data)
|
|
241
|
+
else:
|
|
242
|
+
print(f"DNSSEC deactivated for {zone_name}.")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.command("statistics")
|
|
246
|
+
def zone_statistics(
|
|
247
|
+
zone_name: str = typer.Argument(..., help="Zone name"),
|
|
248
|
+
token: Optional[str] = typer.Option(None, "--token", help="Override token"),
|
|
249
|
+
):
|
|
250
|
+
"""Show zone statistics (query counts)."""
|
|
251
|
+
c = get_client(token)
|
|
252
|
+
data = sdk_call(c.dns.statistics, zone_name)
|
|
253
|
+
if is_json_mode():
|
|
254
|
+
print_json(data)
|
|
255
|
+
else:
|
|
256
|
+
from ..output import print_object
|
|
257
|
+
print_object(data, [
|
|
258
|
+
("total_queries", "Total Queries"), ("queries_today", "Queries Today"),
|
|
259
|
+
("record_count", "Records"),
|
|
260
|
+
])
|