wayscloud-cli 0.1.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wayscloud_cli-0.2.3/LICENSE +21 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/PKG-INFO +6 -2
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/pyproject.toml +8 -2
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/setup.cfg +1 -1
- wayscloud_cli-0.2.3/wayscloud_cli/__init__.py +1 -0
- wayscloud_cli-0.2.3/wayscloud_cli/commands/vps.py +508 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/PKG-INFO +6 -2
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/SOURCES.txt +1 -1
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/top_level.txt +0 -1
- wayscloud_cli-0.1.1/wayscloud_cli/__init__.py +0 -2
- wayscloud_cli-0.1.1/wayscloud_cli/commands/__init__.py +0 -0
- wayscloud_cli-0.1.1/wayscloud_cli/commands/vps.py +0 -206
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/README.md +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/tests/test_smoke.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/__main__.py +0 -0
- {wayscloud_cli-0.1.1/tests → wayscloud_cli-0.2.3/wayscloud_cli/commands}/__init__.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/app.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/db.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/dns.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/iot.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/login.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/redis.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/shell.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/storage.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/config.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/output.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/sdk.py +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/dependency_links.txt +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/entry_points.txt +0 -0
- {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/requires.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WAYSCloud AS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wayscloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.3
|
|
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/#wayscloud-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
|
|
@@ -17,10 +19,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
19
|
Classifier: Operating System :: OS Independent
|
|
18
20
|
Requires-Python: >=3.9
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
20
23
|
Requires-Dist: wayscloud>=0.1.0
|
|
21
24
|
Requires-Dist: typer>=0.9
|
|
22
25
|
Requires-Dist: rich>=13.0
|
|
23
26
|
Requires-Dist: websockets>=12.0
|
|
27
|
+
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|
# WAYSCloud CLI
|
|
26
30
|
|
|
@@ -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.2.3"
|
|
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/#wayscloud-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"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -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.
|
|
3
|
+
Version: 0.2.3
|
|
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/#wayscloud-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
|
|
@@ -17,10 +19,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
19
|
Classifier: Operating System :: OS Independent
|
|
18
20
|
Requires-Python: >=3.9
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
20
23
|
Requires-Dist: wayscloud>=0.1.0
|
|
21
24
|
Requires-Dist: typer>=0.9
|
|
22
25
|
Requires-Dist: rich>=13.0
|
|
23
26
|
Requires-Dist: websockets>=12.0
|
|
27
|
+
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|
# WAYSCloud CLI
|
|
26
30
|
|
|
File without changes
|
|
@@ -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
|
|
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
|