wayscloud-cli 0.1.1__tar.gz → 0.2.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.
Files changed (29) hide show
  1. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/PKG-INFO +4 -2
  2. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/pyproject.toml +8 -2
  3. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/setup.cfg +1 -1
  4. wayscloud_cli-0.2.0/wayscloud_cli/__init__.py +1 -0
  5. wayscloud_cli-0.2.0/wayscloud_cli/commands/vps.py +508 -0
  6. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/PKG-INFO +4 -2
  7. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/top_level.txt +0 -1
  8. wayscloud_cli-0.1.1/wayscloud_cli/__init__.py +0 -2
  9. wayscloud_cli-0.1.1/wayscloud_cli/commands/vps.py +0 -206
  10. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/README.md +0 -0
  11. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/tests/__init__.py +0 -0
  12. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/tests/test_smoke.py +0 -0
  13. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/__main__.py +0 -0
  14. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/__init__.py +0 -0
  15. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/app.py +0 -0
  16. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/db.py +0 -0
  17. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/dns.py +0 -0
  18. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/iot.py +0 -0
  19. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/login.py +0 -0
  20. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/redis.py +0 -0
  21. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/shell.py +0 -0
  22. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/commands/storage.py +0 -0
  23. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/config.py +0 -0
  24. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/output.py +0 -0
  25. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli/sdk.py +0 -0
  26. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/SOURCES.txt +0 -0
  27. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/dependency_links.txt +0 -0
  28. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/entry_points.txt +0 -0
  29. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.0}/wayscloud_cli.egg-info/requires.txt +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wayscloud-cli
3
- Version: 0.1.1
3
+ Version: 0.2.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
7
- Project-URL: Documentation, https://docs.wayscloud.services/cli
7
+ Project-URL: Documentation, https://docs.wayscloud.services/integrations/cli
8
+ Project-URL: Repository, https://github.com/wayscloudas/wayscloud-provision-api
9
+ Project-URL: Issues, https://github.com/wayscloudas/wayscloud-provision-api/issues
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: Intended Audience :: System Administrators
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wayscloud-cli"
7
- version = "0.1.1"
7
+ version = "0.2.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"
@@ -30,7 +30,13 @@ dependencies = [
30
30
 
31
31
  [project.urls]
32
32
  Homepage = "https://wayscloud.services"
33
- Documentation = "https://docs.wayscloud.services/cli"
33
+ Documentation = "https://docs.wayscloud.services/integrations/cli"
34
+ Repository = "https://github.com/wayscloudas/wayscloud-provision-api"
35
+ Issues = "https://github.com/wayscloudas/wayscloud-provision-api/issues"
36
+
37
+ [tool.setuptools.packages.find]
38
+ include = ["wayscloud_cli*"]
39
+ exclude = ["tests*"]
34
40
 
35
41
  [project.scripts]
36
42
  cloud = "wayscloud_cli.__main__:main"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = wayscloud-cli
3
- version = 0.1.0
3
+ version = 0.2.0
4
4
 
5
5
  [options]
6
6
  packages = find:
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,508 @@
1
+ """
2
+ cloud vps commands — VPS provisioning, control, snapshots, and backups.
3
+ """
4
+
5
+ import typer
6
+ from typing import Optional
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="Virtual Private Servers")
12
+
13
+ # ── Sub-apps for snapshots and backups ────────────────────────────
14
+ snapshots_app = typer.Typer(help="VPS snapshots (point-in-time disk copies)")
15
+ backups_app = typer.Typer(help="VPS backups (off-site cloud storage)")
16
+ backups_policy_app = typer.Typer(help="Backup policy management")
17
+
18
+ app.add_typer(snapshots_app, name="snapshots")
19
+ app.add_typer(backups_app, name="backups")
20
+ backups_app.add_typer(backups_policy_app, name="policy")
21
+
22
+
23
+ # ══════════════════════════════════════════════════════════════════
24
+ # VPS Instance Commands
25
+ # ══════════════════════════════════════════════════════════════════
26
+
27
+ @app.command("list")
28
+ def list_vps(
29
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
30
+ status: Optional[str] = typer.Option(None, help="Filter by status"),
31
+ region: Optional[str] = typer.Option(None, help="Filter by region"),
32
+ ):
33
+ """List your VPS instances."""
34
+ c = get_client(token)
35
+ data = sdk_call(c.vps.list, status=status, region=region)
36
+ instances = data if isinstance(data, list) else data.get("instances", []) if isinstance(data, dict) else []
37
+
38
+ if is_json_mode():
39
+ print_json(instances)
40
+ return
41
+
42
+ if not instances:
43
+ print("No VPS instances found.")
44
+ return
45
+
46
+ rows = []
47
+ for v in instances:
48
+ rows.append({
49
+ "id": str(v.get("id", ""))[:12],
50
+ "hostname": v.get("hostname", v.get("display_name", "")),
51
+ "status": v.get("status", ""),
52
+ "ip": v.get("ipv4_address", v.get("ip_address", "")),
53
+ "plan": v.get("plan_code", ""),
54
+ "region": v.get("region", ""),
55
+ "snap": "yes" if v.get("snapshot_supported") else "no",
56
+ })
57
+
58
+ print_table(rows, [
59
+ ("id", "ID", 14),
60
+ ("hostname", "Hostname", 24),
61
+ ("status", "Status", 12),
62
+ ("ip", "IP", 16),
63
+ ("plan", "Plan", 16),
64
+ ("region", "Region", 8),
65
+ ("snap", "Snap", 5),
66
+ ])
67
+
68
+
69
+ @app.command("create")
70
+ def create_vps(
71
+ hostname: str = typer.Option(..., "--hostname", help="VPS hostname"),
72
+ plan: str = typer.Option(..., "--plan", help="Plan code"),
73
+ region: str = typer.Option(..., "--region", help="Region code"),
74
+ os: str = typer.Option(..., "--os", help="OS template"),
75
+ ssh_key: Optional[list[str]] = typer.Option(None, "--ssh-key", help="SSH key (repeatable)"),
76
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
77
+ ):
78
+ """Create a new VPS."""
79
+ c = get_client(token)
80
+ data = sdk_call(c.vps.create, hostname=hostname, plan=plan, region=region,
81
+ os_template=os, ssh_keys=ssh_key or [])
82
+ if is_json_mode():
83
+ print_json(data)
84
+ else:
85
+ print(f"VPS created: {data.get('id', 'unknown')}")
86
+ print_object(data, [("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
87
+ ("plan_code", "Plan"), ("region", "Region")])
88
+
89
+
90
+ @app.command("info")
91
+ def info_vps(
92
+ vps_id: str = typer.Argument(..., help="VPS ID"),
93
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
94
+ ):
95
+ """Show VPS details (status, IPs, plan — all in one)."""
96
+ c = get_client(token)
97
+ data = sdk_call(c.vps.get, vps_id)
98
+
99
+ if is_json_mode():
100
+ print_json(data)
101
+ return
102
+
103
+ print_object(data, [
104
+ ("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
105
+ ("power_state", "Power"), ("ipv4_address", "IPv4"), ("ipv6_address", "IPv6"),
106
+ ("os_template", "OS"), ("plan_code", "Plan"), ("region", "Region"),
107
+ ("disk_format", "Disk Format"), ("snapshot_supported", "Snapshots"),
108
+ ("created_at", "Created"),
109
+ ])
110
+
111
+
112
+ @app.command("delete")
113
+ def delete_vps(
114
+ vps_id: str = typer.Argument(..., help="VPS ID"),
115
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
116
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
117
+ ):
118
+ """Delete a VPS (permanent)."""
119
+ if not confirm:
120
+ print(f"This will permanently delete VPS {vps_id} and all its data.")
121
+ print("Use --confirm to proceed.")
122
+ raise typer.Exit(code=1)
123
+
124
+ c = get_client(token)
125
+ data = sdk_call(c.vps.delete, vps_id)
126
+ if is_json_mode():
127
+ print_json(data)
128
+ else:
129
+ print(f"VPS {vps_id}: deleted")
130
+
131
+
132
+ @app.command("start")
133
+ def start_vps(
134
+ vps_id: str = typer.Argument(..., help="VPS ID"),
135
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
136
+ ):
137
+ """Start a VPS."""
138
+ c = get_client(token)
139
+ data = sdk_call(c.vps.start, vps_id)
140
+ if is_json_mode():
141
+ print_json(data)
142
+ else:
143
+ print(f"VPS {vps_id}: {data.get('message', 'starting')}")
144
+
145
+
146
+ @app.command("stop")
147
+ def stop_vps(
148
+ vps_id: str = typer.Argument(..., help="VPS ID"),
149
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
150
+ ):
151
+ """Stop a VPS."""
152
+ c = get_client(token)
153
+ data = sdk_call(c.vps.stop, vps_id)
154
+ if is_json_mode():
155
+ print_json(data)
156
+ else:
157
+ print(f"VPS {vps_id}: {data.get('message', 'stopping')}")
158
+
159
+
160
+ @app.command("reboot")
161
+ def reboot_vps(
162
+ vps_id: str = typer.Argument(..., help="VPS ID"),
163
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
164
+ ):
165
+ """Reboot a VPS."""
166
+ c = get_client(token)
167
+ data = sdk_call(c.vps.reboot, vps_id)
168
+ if is_json_mode():
169
+ print_json(data)
170
+ else:
171
+ print(f"VPS {vps_id}: {data.get('message', 'rebooting')}")
172
+
173
+
174
+ @app.command("plans")
175
+ def list_plans(
176
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
177
+ region: Optional[str] = typer.Option(None, help="Filter by region"),
178
+ ):
179
+ """List available VPS plans."""
180
+ c = get_client(token)
181
+ plans = sdk_call(c.vps.plans, region=region)
182
+ plans = plans if isinstance(plans, list) else []
183
+
184
+ if is_json_mode():
185
+ print_json(plans)
186
+ return
187
+
188
+ rows = []
189
+ for p in plans:
190
+ rows.append({
191
+ "code": p.get("plan_code", ""),
192
+ "vcpu": p.get("vcpu", ""),
193
+ "ram": f"{p.get('ram_mb', 0) // 1024}GB",
194
+ "disk": f"{p.get('disk_gb', 0)}GB",
195
+ "price": f"{p.get('monthly_price', '')} {p.get('currency', '')}",
196
+ })
197
+
198
+ print_table(rows, [
199
+ ("code", "Plan", 24), ("vcpu", "vCPU", 6), ("ram", "RAM", 8),
200
+ ("disk", "Disk", 8), ("price", "Price/mo", 14),
201
+ ])
202
+
203
+
204
+ @app.command("os-templates")
205
+ def vps_os_templates(
206
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
207
+ ):
208
+ """List available OS templates."""
209
+ c = get_client(token)
210
+ data = sdk_call(c.vps.os_templates)
211
+ templates = data if isinstance(data, list) else data.get("templates", []) if isinstance(data, dict) else []
212
+
213
+ if is_json_mode():
214
+ print_json(templates)
215
+ return
216
+
217
+ rows = []
218
+ for tpl in templates:
219
+ rows.append({
220
+ "id": tpl.get("id", tpl.get("template_id", "")),
221
+ "name": tpl.get("name", ""),
222
+ "os_family": tpl.get("os_family", ""),
223
+ })
224
+
225
+ print_table(rows, [
226
+ ("id", "Template ID", 24), ("name", "Name", 30), ("os_family", "OS Family", 14),
227
+ ])
228
+
229
+
230
+ # ══════════════════════════════════════════════════════════════════
231
+ # Snapshot Commands (cloud vps snapshots ...)
232
+ # ══════════════════════════════════════════════════════════════════
233
+
234
+ @snapshots_app.command("list")
235
+ def snapshots_list(
236
+ vps_id: str = typer.Argument(..., help="VPS ID"),
237
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
238
+ ):
239
+ """List snapshots for a VPS."""
240
+ c = get_client(token)
241
+ snaps = sdk_call(c.vps.snapshots, vps_id)
242
+
243
+ if is_json_mode():
244
+ print_json(snaps)
245
+ return
246
+
247
+ if not snaps:
248
+ print("No snapshots.")
249
+ return
250
+
251
+ rows = []
252
+ for s in snaps:
253
+ rows.append({
254
+ "name": s.get("snapshot_name", ""),
255
+ "status": s.get("status", ""),
256
+ "created": s.get("created_at", "")[:19],
257
+ "desc": (s.get("description") or "")[:30],
258
+ })
259
+
260
+ print_table(rows, [
261
+ ("name", "Name", 24), ("status", "Status", 12),
262
+ ("created", "Created", 20), ("desc", "Description", 32),
263
+ ])
264
+
265
+
266
+ @snapshots_app.command("create")
267
+ def snapshots_create(
268
+ vps_id: str = typer.Argument(..., help="VPS ID"),
269
+ name: str = typer.Option(..., "--name", help="Snapshot name"),
270
+ description: Optional[str] = typer.Option(None, "--description", help="Description"),
271
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
272
+ ):
273
+ """Create a point-in-time snapshot. Not a backup — stored on same server."""
274
+ c = get_client(token)
275
+ data = sdk_call(c.vps.create_snapshot, vps_id, name=name, description=description)
276
+ if is_json_mode():
277
+ print_json(data)
278
+ else:
279
+ print(f"Snapshot created: {data.get('snapshot_name', name)} (status: {data.get('status', '?')})")
280
+
281
+
282
+ @snapshots_app.command("delete")
283
+ def snapshots_delete(
284
+ vps_id: str = typer.Argument(..., help="VPS ID"),
285
+ name: str = typer.Option(..., "--name", help="Snapshot name"),
286
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
287
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
288
+ ):
289
+ """Delete a snapshot."""
290
+ if not confirm:
291
+ print(f"This will permanently delete snapshot '{name}' from VPS {vps_id}.")
292
+ print("Use --confirm to proceed.")
293
+ raise typer.Exit(code=1)
294
+
295
+ c = get_client(token)
296
+ data = sdk_call(c.vps.delete_snapshot, vps_id, name)
297
+ if is_json_mode():
298
+ print_json(data)
299
+ else:
300
+ print(f"Snapshot '{name}' deleted.")
301
+
302
+
303
+ @snapshots_app.command("rollback")
304
+ def snapshots_rollback(
305
+ vps_id: str = typer.Argument(..., help="VPS ID"),
306
+ name: str = typer.Option(..., "--name", help="Snapshot name to rollback to"),
307
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
308
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
309
+ ):
310
+ """Rollback VPS to a snapshot. DESTRUCTIVE — data after snapshot is lost. VPS must be stopped."""
311
+ if not confirm:
312
+ print(f"WARNING: This will rollback VPS {vps_id} to snapshot '{name}'.")
313
+ print("All data written after the snapshot will be PERMANENTLY LOST.")
314
+ print("The VPS must be stopped before rollback.")
315
+ print("Use --confirm to proceed.")
316
+ raise typer.Exit(code=1)
317
+
318
+ c = get_client(token)
319
+ data = sdk_call(c.vps.rollback_snapshot, vps_id, name)
320
+ if is_json_mode():
321
+ print_json(data)
322
+ else:
323
+ print(f"Rollback complete. Start the VPS to resume.")
324
+
325
+
326
+ # ══════════════════════════════════════════════════════════════════
327
+ # Backup Commands (cloud vps backups ...)
328
+ # ══════════════════════════════════════════════════════════════════
329
+
330
+ @backups_app.command("list")
331
+ def backups_list(
332
+ vps_id: str = typer.Argument(..., help="VPS ID"),
333
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
334
+ ):
335
+ """List off-site backups for a VPS."""
336
+ c = get_client(token)
337
+ data = sdk_call(c.vps.backups, vps_id)
338
+
339
+ if is_json_mode():
340
+ print_json(data)
341
+ return
342
+
343
+ if not data:
344
+ print("No backups.")
345
+ return
346
+
347
+ rows = []
348
+ for b in data:
349
+ rows.append({
350
+ "id": str(b.get("backup_id", ""))[:12],
351
+ "type": b.get("backup_type", ""),
352
+ "size": f"{b.get('size_gb', 0) or 0:.2f} GB",
353
+ "status": b.get("status", ""),
354
+ "created": b.get("started_at", "")[:19],
355
+ "expires": (b.get("expires_at") or "")[:10],
356
+ })
357
+
358
+ print_table(rows, [
359
+ ("id", "ID", 14), ("type", "Type", 10), ("size", "Size", 10),
360
+ ("status", "Status", 12), ("created", "Created", 20), ("expires", "Expires", 12),
361
+ ])
362
+
363
+
364
+ @backups_app.command("create")
365
+ def backups_create(
366
+ vps_id: str = typer.Argument(..., help="VPS ID"),
367
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
368
+ ):
369
+ """Trigger a manual off-site backup. Runs asynchronously."""
370
+ c = get_client(token)
371
+ data = sdk_call(c.vps.create_backup, vps_id)
372
+ if is_json_mode():
373
+ print_json(data)
374
+ else:
375
+ print(f"Backup queued: {data.get('backup_id', '?')} (status: {data.get('status', 'creating')})")
376
+ print("Use 'cloud vps backups list' to check progress.")
377
+
378
+
379
+ @backups_app.command("delete")
380
+ def backups_delete(
381
+ vps_id: str = typer.Argument(..., help="VPS ID"),
382
+ backup_id: str = typer.Option(..., "--backup-id", help="Backup ID"),
383
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
384
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
385
+ ):
386
+ """Delete a backup from off-site storage (permanent)."""
387
+ if not confirm:
388
+ print(f"This will permanently delete backup {backup_id}.")
389
+ print("Use --confirm to proceed.")
390
+ raise typer.Exit(code=1)
391
+
392
+ c = get_client(token)
393
+ data = sdk_call(c.vps.delete_backup, vps_id, backup_id)
394
+ if is_json_mode():
395
+ print_json(data)
396
+ else:
397
+ print("Backup deleted.")
398
+
399
+
400
+ @backups_app.command("restore")
401
+ def backups_restore(
402
+ vps_id: str = typer.Argument(..., help="VPS ID"),
403
+ backup_id: str = typer.Option(..., "--backup-id", help="Backup ID"),
404
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
405
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
406
+ ):
407
+ """Restore VPS from backup. DESTRUCTIVE — replaces current disk. VPS must be stopped."""
408
+ if not confirm:
409
+ print(f"WARNING: This will replace VPS {vps_id} disk with backup {backup_id}.")
410
+ print("All current data will be PERMANENTLY LOST.")
411
+ print("The VPS must be stopped before restore.")
412
+ print("Use --confirm to proceed.")
413
+ raise typer.Exit(code=1)
414
+
415
+ c = get_client(token)
416
+ # Note: restore currently returns 501 until restore worker is built.
417
+ data = sdk_call(c.vps.restore_backup, vps_id, backup_id)
418
+ if is_json_mode():
419
+ print_json(data)
420
+ else:
421
+ print(data.get("message", "Restore queued."))
422
+
423
+
424
+ @backups_app.command("usage")
425
+ def backups_usage(
426
+ vps_id: str = typer.Argument(..., help="VPS ID"),
427
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
428
+ ):
429
+ """Show backup storage usage for a VPS."""
430
+ c = get_client(token)
431
+ data = sdk_call(c.vps.backup_usage, vps_id)
432
+ if is_json_mode():
433
+ print_json(data)
434
+ else:
435
+ print_object(data, [
436
+ ("total_backups", "Backups"), ("total_size_gb", "Total Size (GB)"),
437
+ ])
438
+
439
+
440
+ # ── Backup Policy (cloud vps backups policy ...) ─────────────────
441
+
442
+ @backups_policy_app.command("get")
443
+ def policy_get(
444
+ vps_id: str = typer.Argument(..., help="VPS ID"),
445
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
446
+ ):
447
+ """Get backup policy for a VPS."""
448
+ c = get_client(token)
449
+ data = sdk_call(c.vps.backup_policy, vps_id)
450
+ policy = data.get("policy") if isinstance(data, dict) else data
451
+
452
+ if is_json_mode():
453
+ print_json(policy)
454
+ return
455
+
456
+ if not policy:
457
+ print("No backup policy configured.")
458
+ return
459
+
460
+ print_object(policy, [
461
+ ("enabled", "Enabled"), ("frequency", "Frequency"),
462
+ ("time_of_day", "Time (UTC)"), ("day_of_week", "Day of Week"),
463
+ ("retention_days", "Retention (days)"),
464
+ ("next_backup_at", "Next Backup"), ("last_backup_at", "Last Backup"),
465
+ ])
466
+
467
+
468
+ @backups_policy_app.command("set")
469
+ def policy_set(
470
+ vps_id: str = typer.Argument(..., help="VPS ID"),
471
+ frequency: str = typer.Option("daily", "--frequency", help="daily or weekly"),
472
+ time: str = typer.Option("03:00", "--time", help="Time of day (UTC, HH:MM)"),
473
+ retention: int = typer.Option(7, "--retention", help="Days to keep backups (1-90)"),
474
+ day: Optional[int] = typer.Option(None, "--day", help="Day of week (0=Mon, for weekly)"),
475
+ enabled: bool = typer.Option(True, "--enabled/--disabled", help="Enable/disable"),
476
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
477
+ ):
478
+ """Set or update backup policy for a VPS."""
479
+ c = get_client(token)
480
+ data = sdk_call(c.vps.set_backup_policy, vps_id,
481
+ enabled=enabled, frequency=frequency,
482
+ time_of_day=time, day_of_week=day,
483
+ retention_days=retention)
484
+ if is_json_mode():
485
+ print_json(data)
486
+ else:
487
+ print(f"Backup policy saved. Next backup: {data.get('next_backup_at', '?')}")
488
+
489
+
490
+ @backups_policy_app.command("remove")
491
+ def policy_remove(
492
+ vps_id: str = typer.Argument(..., help="VPS ID"),
493
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
494
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
495
+ ):
496
+ """Remove backup policy (stops automatic backups)."""
497
+ if not confirm:
498
+ print("This will remove the backup policy and stop automatic backups.")
499
+ print("Existing backups will not be deleted.")
500
+ print("Use --confirm to proceed.")
501
+ raise typer.Exit(code=1)
502
+
503
+ c = get_client(token)
504
+ data = sdk_call(c.vps.delete_backup_policy, vps_id)
505
+ if is_json_mode():
506
+ print_json(data)
507
+ else:
508
+ print("Backup policy removed.")
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wayscloud-cli
3
- Version: 0.1.1
3
+ Version: 0.2.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
7
- Project-URL: Documentation, https://docs.wayscloud.services/cli
7
+ Project-URL: Documentation, https://docs.wayscloud.services/integrations/cli
8
+ Project-URL: Repository, https://github.com/wayscloudas/wayscloud-provision-api
9
+ Project-URL: Issues, https://github.com/wayscloudas/wayscloud-provision-api/issues
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: Intended Audience :: System Administrators
@@ -1,2 +0,0 @@
1
- """WAYSCloud CLI — cloud command."""
2
- __version__ = "0.1.1"
@@ -1,206 +0,0 @@
1
- """
2
- cloud vps commands — VPS provisioning and control.
3
- """
4
-
5
- import typer
6
- from typing import Optional
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="Virtual Private Servers")
12
-
13
-
14
- @app.command("list")
15
- def list_vps(
16
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
17
- status: Optional[str] = typer.Option(None, help="Filter by status"),
18
- region: Optional[str] = typer.Option(None, help="Filter by region"),
19
- ):
20
- """List your VPS instances."""
21
- c = get_client(token)
22
- data = sdk_call(c.vps.list, status=status, region=region)
23
- instances = data if isinstance(data, list) else data.get("instances", []) if isinstance(data, dict) else []
24
-
25
- if is_json_mode():
26
- print_json(instances)
27
- return
28
-
29
- if not instances:
30
- print("No VPS instances found.")
31
- return
32
-
33
- rows = []
34
- for v in instances:
35
- rows.append({
36
- "id": str(v.get("id", ""))[:12],
37
- "hostname": v.get("hostname", v.get("display_name", "")),
38
- "status": v.get("status", ""),
39
- "ip": v.get("ipv4_address", v.get("ip_address", "")),
40
- "plan": v.get("plan_code", ""),
41
- "region": v.get("region", ""),
42
- })
43
-
44
- print_table(rows, [
45
- ("id", "ID", 14),
46
- ("hostname", "Hostname", 24),
47
- ("status", "Status", 12),
48
- ("ip", "IP", 16),
49
- ("plan", "Plan", 16),
50
- ("region", "Region", 8),
51
- ])
52
-
53
-
54
- @app.command("create")
55
- def create_vps(
56
- hostname: str = typer.Option(..., "--hostname", help="VPS hostname"),
57
- plan: str = typer.Option(..., "--plan", help="Plan code"),
58
- region: str = typer.Option(..., "--region", help="Region code"),
59
- os: str = typer.Option(..., "--os", help="OS template"),
60
- ssh_key: Optional[list[str]] = typer.Option(None, "--ssh-key", help="SSH key (repeatable)"),
61
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
62
- ):
63
- """Create a new VPS."""
64
- c = get_client(token)
65
- data = sdk_call(c.vps.create, hostname=hostname, plan=plan, region=region,
66
- os_template=os, ssh_keys=ssh_key or [])
67
- if is_json_mode():
68
- print_json(data)
69
- else:
70
- print(f"VPS created: {data.get('id', 'unknown')}")
71
- print_object(data, [("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
72
- ("plan_code", "Plan"), ("region", "Region")])
73
-
74
-
75
- @app.command("info")
76
- def info_vps(
77
- vps_id: str = typer.Argument(..., help="VPS ID"),
78
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
79
- ):
80
- """Show VPS details (status, IPs, plan — all in one)."""
81
- c = get_client(token)
82
- data = sdk_call(c.vps.get, vps_id)
83
- print_object(data, [
84
- ("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
85
- ("power_state", "Power"), ("ipv4_address", "IPv4"), ("ipv6_address", "IPv6"),
86
- ("os_template", "OS"), ("plan_code", "Plan"), ("region", "Region"),
87
- ("created_at", "Created"),
88
- ])
89
-
90
-
91
- @app.command("delete")
92
- def delete_vps(
93
- vps_id: str = typer.Argument(..., help="VPS ID"),
94
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
95
- confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
96
- ):
97
- """Delete a VPS (permanent)."""
98
- if not confirm:
99
- print(f"This will permanently delete VPS {vps_id} and all its data.")
100
- print("Use --confirm to proceed.")
101
- raise typer.Exit(code=1)
102
-
103
- c = get_client(token)
104
- data = sdk_call(c.vps.delete, vps_id)
105
- if is_json_mode():
106
- print_json(data)
107
- else:
108
- print(f"VPS {vps_id}: deleted")
109
-
110
-
111
- @app.command("start")
112
- def start_vps(
113
- vps_id: str = typer.Argument(..., help="VPS ID"),
114
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
115
- ):
116
- """Start a VPS."""
117
- c = get_client(token)
118
- data = sdk_call(c.vps.start, vps_id)
119
- if is_json_mode():
120
- print_json(data)
121
- else:
122
- print(f"VPS {vps_id}: {data.get('message', 'starting')}")
123
-
124
-
125
- @app.command("stop")
126
- def stop_vps(
127
- vps_id: str = typer.Argument(..., help="VPS ID"),
128
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
129
- ):
130
- """Stop a VPS."""
131
- c = get_client(token)
132
- data = sdk_call(c.vps.stop, vps_id)
133
- if is_json_mode():
134
- print_json(data)
135
- else:
136
- print(f"VPS {vps_id}: {data.get('message', 'stopping')}")
137
-
138
-
139
- @app.command("reboot")
140
- def reboot_vps(
141
- vps_id: str = typer.Argument(..., help="VPS ID"),
142
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
143
- ):
144
- """Reboot a VPS."""
145
- c = get_client(token)
146
- data = sdk_call(c.vps.reboot, vps_id)
147
- if is_json_mode():
148
- print_json(data)
149
- else:
150
- print(f"VPS {vps_id}: {data.get('message', 'rebooting')}")
151
-
152
-
153
- @app.command("plans")
154
- def list_plans(
155
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
156
- region: Optional[str] = typer.Option(None, help="Filter by region"),
157
- ):
158
- """List available VPS plans."""
159
- c = get_client(token)
160
- plans = sdk_call(c.vps.plans, region=region)
161
- plans = plans if isinstance(plans, list) else []
162
-
163
- if is_json_mode():
164
- print_json(plans)
165
- return
166
-
167
- rows = []
168
- for p in plans:
169
- rows.append({
170
- "code": p.get("plan_code", ""),
171
- "vcpu": p.get("vcpu", ""),
172
- "ram": f"{p.get('ram_mb', 0) // 1024}GB",
173
- "disk": f"{p.get('disk_gb', 0)}GB",
174
- "price": f"{p.get('monthly_price', '')} {p.get('currency', '')}",
175
- })
176
-
177
- print_table(rows, [
178
- ("code", "Plan", 24), ("vcpu", "vCPU", 6), ("ram", "RAM", 8),
179
- ("disk", "Disk", 8), ("price", "Price/mo", 14),
180
- ])
181
-
182
-
183
- @app.command("os-templates")
184
- def vps_os_templates(
185
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
186
- ):
187
- """List available OS templates."""
188
- c = get_client(token)
189
- data = sdk_call(c.vps.os_templates)
190
- templates = data if isinstance(data, list) else data.get("templates", []) if isinstance(data, dict) else []
191
-
192
- if is_json_mode():
193
- print_json(templates)
194
- return
195
-
196
- rows = []
197
- for tpl in templates:
198
- rows.append({
199
- "id": tpl.get("id", tpl.get("template_id", "")),
200
- "name": tpl.get("name", ""),
201
- "os_family": tpl.get("os_family", ""),
202
- })
203
-
204
- print_table(rows, [
205
- ("id", "Template ID", 24), ("name", "Name", 30), ("os_family", "OS Family", 14),
206
- ])
File without changes