compshare-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compshare_cli/__init__.py +3 -0
- compshare_cli/__main__.py +4 -0
- compshare_cli/actions.py +71 -0
- compshare_cli/api.py +127 -0
- compshare_cli/cli.py +219 -0
- compshare_cli/commands/__init__.py +1 -0
- compshare_cli/commands/common.py +57 -0
- compshare_cli/commands/image.py +528 -0
- compshare_cli/commands/instance.py +1493 -0
- compshare_cli/commands/storage.py +276 -0
- compshare_cli/config.py +127 -0
- compshare_cli/errors.py +10 -0
- compshare_cli/i18n.py +506 -0
- compshare_cli/location.py +91 -0
- compshare_cli/output.py +124 -0
- compshare_cli/parsing.py +93 -0
- compshare_cli/runtime.py +34 -0
- compshare_cli/sdk.py +27 -0
- compshare_cli-0.1.0.dist-info/METADATA +283 -0
- compshare_cli-0.1.0.dist-info/RECORD +23 -0
- compshare_cli-0.1.0.dist-info/WHEEL +5 -0
- compshare_cli-0.1.0.dist-info/entry_points.txt +2 -0
- compshare_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, TypeVar
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from compshare_cli.api import call, invoke
|
|
12
|
+
from compshare_cli.commands.common import confirm, confirm_details, request, runtime
|
|
13
|
+
from compshare_cli.errors import UsageError
|
|
14
|
+
from compshare_cli.i18n import tr
|
|
15
|
+
from compshare_cli.location import locate_instance, region_from_zone, supported_regions
|
|
16
|
+
from compshare_cli.output import Renderer
|
|
17
|
+
from compshare_cli.parsing import compact, disk_gib, encode_password, memory_mib, timestamp
|
|
18
|
+
from compshare_cli.runtime import Runtime
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help="Manage GPU instances.", no_args_is_help=True)
|
|
21
|
+
ports_app = typer.Typer(help="Manage container port mappings.", no_args_is_help=True)
|
|
22
|
+
schedule_app = typer.Typer(help="Manage scheduled shutdowns.", no_args_is_help=True)
|
|
23
|
+
software_app = typer.Typer(help="Discover software exposed by instances.", no_args_is_help=True)
|
|
24
|
+
app.add_typer(ports_app, name="ports")
|
|
25
|
+
app.add_typer(schedule_app, name="schedule")
|
|
26
|
+
app.add_typer(software_app, name="software")
|
|
27
|
+
|
|
28
|
+
INSTANCE_COLUMNS = (
|
|
29
|
+
("UHostId", "ID"),
|
|
30
|
+
("Name", "NAME"),
|
|
31
|
+
("State", "STATE"),
|
|
32
|
+
("GpuType", "GPU"),
|
|
33
|
+
("GPU", "COUNT"),
|
|
34
|
+
("CPU", "CPU"),
|
|
35
|
+
("MemoryDisplay", "MEMORY"),
|
|
36
|
+
("Region", "REGION"),
|
|
37
|
+
("Zone", "ZONE"),
|
|
38
|
+
("ChargeType", "CHARGE"),
|
|
39
|
+
("InstancePrice", "PRICE/H"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Choice = TypeVar("Choice")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _instance_rows(response: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
|
|
46
|
+
for raw in response.get("UHostSet", []):
|
|
47
|
+
row = dict(raw)
|
|
48
|
+
memory = row.get("Memory")
|
|
49
|
+
row["MemoryDisplay"] = f"{memory // 1024}GiB" if isinstance(memory, int) else memory
|
|
50
|
+
yield row
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _search_rows(
|
|
54
|
+
response: Dict[str, Any],
|
|
55
|
+
inventory: Optional[Dict[str, Dict[tuple, bool]]] = None,
|
|
56
|
+
available_only: bool = False,
|
|
57
|
+
) -> Iterable[Dict[str, Any]]:
|
|
58
|
+
inventory = inventory or {}
|
|
59
|
+
for machine in response.get("AvailableInstanceTypes", []):
|
|
60
|
+
gpu_memory = machine.get("GraphicsMemory", {}).get("Value")
|
|
61
|
+
for size in machine.get("MachineSizes", []):
|
|
62
|
+
for collection in size.get("Collection", []):
|
|
63
|
+
for memory in collection.get("Memory", []):
|
|
64
|
+
stock = inventory.get(machine.get("Name"), {}).get(
|
|
65
|
+
(size.get("Gpu"), collection.get("Cpu"), memory)
|
|
66
|
+
)
|
|
67
|
+
if available_only and stock is not True:
|
|
68
|
+
continue
|
|
69
|
+
yield {
|
|
70
|
+
"GpuType": machine.get("Name"),
|
|
71
|
+
"GPU": size.get("Gpu"),
|
|
72
|
+
"CPU": collection.get("Cpu"),
|
|
73
|
+
"Memory": f"{memory}GiB",
|
|
74
|
+
"VRAM": f"{gpu_memory}GiB" if gpu_memory else None,
|
|
75
|
+
"Zone": machine.get("Zone"),
|
|
76
|
+
"Stock": stock,
|
|
77
|
+
"Platforms": collection.get("MinimalCpuPlatform", []),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _disk_list(
|
|
82
|
+
boot_disk: str,
|
|
83
|
+
boot_type: str,
|
|
84
|
+
data_disks: Optional[List[str]],
|
|
85
|
+
) -> List[Dict[str, Any]]:
|
|
86
|
+
disks: List[Dict[str, Any]] = [{"IsBoot": True, "Type": boot_type, "Size": disk_gib(boot_disk)}]
|
|
87
|
+
for specification in data_disks or []:
|
|
88
|
+
parts = specification.split(":", 1)
|
|
89
|
+
size = disk_gib(parts[0])
|
|
90
|
+
disk_type = parts[1] if len(parts) == 2 else boot_type
|
|
91
|
+
disks.append({"IsBoot": False, "Type": disk_type, "Size": size})
|
|
92
|
+
return disks
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _volume_list(volumes: Optional[List[str]]) -> Optional[List[Dict[str, Any]]]:
|
|
96
|
+
if not volumes:
|
|
97
|
+
return None
|
|
98
|
+
result: List[Dict[str, Any]] = []
|
|
99
|
+
for specification in volumes:
|
|
100
|
+
parts = specification.split(":", 1)
|
|
101
|
+
size = disk_gib(parts[0])
|
|
102
|
+
volume_type = parts[1] if len(parts) == 2 else "UDisk"
|
|
103
|
+
result.append({"Type": volume_type, "Size": size})
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _choose(
|
|
108
|
+
title: str,
|
|
109
|
+
choices: Sequence[Choice],
|
|
110
|
+
label: Callable[[Choice], str],
|
|
111
|
+
*,
|
|
112
|
+
default: int = 1,
|
|
113
|
+
) -> Choice:
|
|
114
|
+
if not choices:
|
|
115
|
+
raise UsageError(f"{tr(title)}: {tr('No selectable options')}")
|
|
116
|
+
typer.echo(f"\n{tr(title)}")
|
|
117
|
+
for index, choice in enumerate(choices, start=1):
|
|
118
|
+
typer.echo(f" {index}. {label(choice)}")
|
|
119
|
+
if len(choices) == 1:
|
|
120
|
+
typer.echo(f" {tr('Automatically selected the only option.')}")
|
|
121
|
+
return choices[0]
|
|
122
|
+
while True:
|
|
123
|
+
selected = typer.prompt(tr("Select"), default=default, type=int)
|
|
124
|
+
if 1 <= selected <= len(choices):
|
|
125
|
+
return choices[selected - 1]
|
|
126
|
+
typer.echo(tr("Please enter a number from 1 to {count}.", count=len(choices)), err=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _wait_enabled(state: Runtime, value: Optional[bool]) -> bool:
|
|
130
|
+
if value is not None:
|
|
131
|
+
return value
|
|
132
|
+
return not state.json_output and sys.stdout.isatty()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _wait_for_instance(
|
|
136
|
+
state: Runtime,
|
|
137
|
+
instance: str,
|
|
138
|
+
*,
|
|
139
|
+
region: str,
|
|
140
|
+
desired: Optional[set[str]] = None,
|
|
141
|
+
absent: bool = False,
|
|
142
|
+
timeout: int = 600,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
started = time.monotonic()
|
|
145
|
+
previous: Optional[str] = None
|
|
146
|
+
while True:
|
|
147
|
+
response = call(
|
|
148
|
+
state,
|
|
149
|
+
"DescribeCompShareInstance",
|
|
150
|
+
{"Region": region, "UHostIds": [instance]},
|
|
151
|
+
)
|
|
152
|
+
hosts = response.get("UHostSet", [])
|
|
153
|
+
if absent and not hosts:
|
|
154
|
+
return response
|
|
155
|
+
current = str(hosts[0].get("State", "Unknown")) if hosts else "NotFound"
|
|
156
|
+
if hosts and desired and current in desired:
|
|
157
|
+
return response
|
|
158
|
+
if (
|
|
159
|
+
hosts
|
|
160
|
+
and desired is None
|
|
161
|
+
and current
|
|
162
|
+
not in {
|
|
163
|
+
"Initializing",
|
|
164
|
+
"Pending",
|
|
165
|
+
"Starting",
|
|
166
|
+
"Stopping",
|
|
167
|
+
"Rebooting",
|
|
168
|
+
"Reinstalling",
|
|
169
|
+
"Resizing",
|
|
170
|
+
}
|
|
171
|
+
):
|
|
172
|
+
return response
|
|
173
|
+
if current != previous and not state.json_output:
|
|
174
|
+
typer.echo(tr("Waiting for {instance}: {state}", instance=instance, state=current))
|
|
175
|
+
previous = current
|
|
176
|
+
if time.monotonic() - started >= timeout:
|
|
177
|
+
raise UsageError(
|
|
178
|
+
tr(
|
|
179
|
+
"Timed out after {timeout}s while waiting for {instance}.",
|
|
180
|
+
timeout=timeout,
|
|
181
|
+
instance=instance,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
time.sleep(3)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _project_id(state: Runtime, explicit: Optional[str]) -> str:
|
|
188
|
+
if explicit:
|
|
189
|
+
return explicit
|
|
190
|
+
response = call(state, "GetProjectList", {})
|
|
191
|
+
projects = response.get("ProjectSet", [])
|
|
192
|
+
selected = next((item for item in projects if item.get("IsDefault")), None)
|
|
193
|
+
selected = selected or (projects[0] if projects else None)
|
|
194
|
+
if not selected or not selected.get("ProjectId"):
|
|
195
|
+
raise UsageError(
|
|
196
|
+
tr("No project was returned by GetProjectList; pass --project-id explicitly.")
|
|
197
|
+
)
|
|
198
|
+
return str(selected["ProjectId"])
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _create_location(
|
|
202
|
+
state: Runtime,
|
|
203
|
+
zone: Optional[str],
|
|
204
|
+
*,
|
|
205
|
+
interactive: bool,
|
|
206
|
+
) -> Tuple[str, str]:
|
|
207
|
+
selected_zone = zone or state.zone
|
|
208
|
+
selected_region = region_from_zone(selected_zone)
|
|
209
|
+
if not interactive or zone is not None:
|
|
210
|
+
return selected_region, selected_zone
|
|
211
|
+
|
|
212
|
+
response = call(
|
|
213
|
+
state,
|
|
214
|
+
"DescribeCompShareSupportZone",
|
|
215
|
+
{"Region": state.region},
|
|
216
|
+
)
|
|
217
|
+
zones = response.get("ZoneInfo", [])
|
|
218
|
+
if not zones:
|
|
219
|
+
return selected_region, selected_zone
|
|
220
|
+
default = next(
|
|
221
|
+
(index for index, item in enumerate(zones, start=1) if item.get("Zone") == selected_zone),
|
|
222
|
+
1,
|
|
223
|
+
)
|
|
224
|
+
selected = _choose(
|
|
225
|
+
"Availability zone",
|
|
226
|
+
zones,
|
|
227
|
+
lambda item: (
|
|
228
|
+
f"{item.get('Describe') or item.get('Zone')} "
|
|
229
|
+
f"({item.get('Region')} / {item.get('Zone')})"
|
|
230
|
+
),
|
|
231
|
+
default=default,
|
|
232
|
+
)
|
|
233
|
+
return selected.get("Region") or selected_region, selected.get("Zone") or selected_zone
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _create_gpu(
|
|
237
|
+
state: Runtime,
|
|
238
|
+
region: str,
|
|
239
|
+
zone: str,
|
|
240
|
+
gpu: Optional[str],
|
|
241
|
+
) -> str:
|
|
242
|
+
if gpu is not None:
|
|
243
|
+
return gpu
|
|
244
|
+
response = call(
|
|
245
|
+
state,
|
|
246
|
+
"DescribeAvailableCompShareInstanceTypes",
|
|
247
|
+
{"Region": region, "Zone": zone, "InstanceType": "uhost"},
|
|
248
|
+
)
|
|
249
|
+
machines = response.get("AvailableInstanceTypes", [])
|
|
250
|
+
|
|
251
|
+
def label(machine: Dict[str, Any]) -> str:
|
|
252
|
+
memory = machine.get("GraphicsMemory", {}).get("Value")
|
|
253
|
+
suffix = f" · {memory}GiB VRAM" if memory else ""
|
|
254
|
+
return f"{machine.get('Name')}{suffix}"
|
|
255
|
+
|
|
256
|
+
selected = _choose("GPU type", machines, label)
|
|
257
|
+
value = selected.get("Name")
|
|
258
|
+
if not value:
|
|
259
|
+
raise UsageError(tr("The machine type API returned an unnamed GPU."))
|
|
260
|
+
return str(value)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _create_images(
|
|
264
|
+
state: Runtime,
|
|
265
|
+
region: str,
|
|
266
|
+
zone: str,
|
|
267
|
+
source: Optional[str],
|
|
268
|
+
) -> List[Dict[str, Any]]:
|
|
269
|
+
sources = ["platform", "custom", "community", "shared"]
|
|
270
|
+
selected_source = (
|
|
271
|
+
source.lower()
|
|
272
|
+
if source
|
|
273
|
+
else _choose(
|
|
274
|
+
"Image source",
|
|
275
|
+
sources,
|
|
276
|
+
lambda value: value,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
mapping = {
|
|
280
|
+
"platform": "DescribeCompShareImages",
|
|
281
|
+
"custom": "DescribeCompShareCustomImages",
|
|
282
|
+
"community": "DescribeCommunityImages",
|
|
283
|
+
"shared": "DescribeCompShareSharingImages",
|
|
284
|
+
}
|
|
285
|
+
if selected_source not in mapping:
|
|
286
|
+
raise UsageError(tr("--image-source must be platform, custom, community, or shared."))
|
|
287
|
+
params: Dict[str, Any] = {"Region": region, "Limit": 100, "Offset": 0}
|
|
288
|
+
if selected_source == "platform":
|
|
289
|
+
params["Zone"] = zone
|
|
290
|
+
response = call(state, mapping[selected_source], params)
|
|
291
|
+
if selected_source == "community":
|
|
292
|
+
images = []
|
|
293
|
+
for group in response.get("CompshareImageGroup", []):
|
|
294
|
+
for raw in group.get("Data", []):
|
|
295
|
+
image = dict(raw)
|
|
296
|
+
image.setdefault("Name", group.get("ImageName"))
|
|
297
|
+
images.append(image)
|
|
298
|
+
else:
|
|
299
|
+
images = list(response.get("ImageSet", []))
|
|
300
|
+
return [
|
|
301
|
+
image
|
|
302
|
+
for image in images
|
|
303
|
+
if image.get("CompShareImageId") and image.get("Status", "Available") == "Available"
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _create_image(
|
|
308
|
+
state: Runtime,
|
|
309
|
+
region: str,
|
|
310
|
+
zone: str,
|
|
311
|
+
image: Optional[str],
|
|
312
|
+
source: Optional[str],
|
|
313
|
+
) -> str:
|
|
314
|
+
if image is not None:
|
|
315
|
+
return image
|
|
316
|
+
images = _create_images(state, region, zone, source)
|
|
317
|
+
if len(images) > 1:
|
|
318
|
+
query = typer.prompt(
|
|
319
|
+
tr("Filter images by name or ID (blank shows all)"),
|
|
320
|
+
default="",
|
|
321
|
+
show_default=False,
|
|
322
|
+
).strip()
|
|
323
|
+
if query:
|
|
324
|
+
normalized = query.casefold()
|
|
325
|
+
images = [
|
|
326
|
+
item
|
|
327
|
+
for item in images
|
|
328
|
+
if normalized in str(item.get("Name", "")).casefold()
|
|
329
|
+
or normalized in str(item.get("CompShareImageId", "")).casefold()
|
|
330
|
+
]
|
|
331
|
+
if len(images) > 50:
|
|
332
|
+
raise UsageError(tr("More than 50 images matched; use a more specific filter."))
|
|
333
|
+
|
|
334
|
+
def label(item: Dict[str, Any]) -> str:
|
|
335
|
+
author = f" · {item.get('Author')}" if item.get("Author") else ""
|
|
336
|
+
return f"{item.get('Name') or tr('Unnamed')}{author} · {item.get('CompShareImageId')}"
|
|
337
|
+
|
|
338
|
+
selected = _choose("Image", images, label)
|
|
339
|
+
return str(selected["CompShareImageId"])
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _create_charge(charge: Optional[str]) -> str:
|
|
343
|
+
if charge is not None:
|
|
344
|
+
return charge
|
|
345
|
+
return _choose(
|
|
346
|
+
"Billing type",
|
|
347
|
+
["Postpay", "Spot", "Day", "Month"],
|
|
348
|
+
lambda value: value,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("search")
|
|
353
|
+
def search(
|
|
354
|
+
ctx: typer.Context,
|
|
355
|
+
gpu: Optional[List[str]] = typer.Option(None, "--gpu", help="GPU type; repeatable."),
|
|
356
|
+
spot: bool = typer.Option(False, "--spot", help="Search interruptible instances."),
|
|
357
|
+
image: Optional[str] = typer.Option(
|
|
358
|
+
None,
|
|
359
|
+
"--image",
|
|
360
|
+
help="Image ID. When set, check real inventory for every matched GPU type.",
|
|
361
|
+
),
|
|
362
|
+
available: bool = typer.Option(False, "--available", help="Show only in-stock specs."),
|
|
363
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Availability zone."),
|
|
364
|
+
platform: str = typer.Option("Auto", "--platform", help="CPU platform for stock checks."),
|
|
365
|
+
charge: Optional[str] = typer.Option(None, "--charge", help="Billing type for stock checks."),
|
|
366
|
+
disk: str = typer.Option("100GiB", "--disk", help="Boot disk size for stock checks."),
|
|
367
|
+
disk_type: str = typer.Option(
|
|
368
|
+
"CLOUD_SSD", "--disk-type", help="Boot disk type used for stock checks."
|
|
369
|
+
),
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Search legal specifications and, with --image, real inventory."""
|
|
372
|
+
if available and image is None:
|
|
373
|
+
raise UsageError(
|
|
374
|
+
tr("--available requires --image because inventory depends on the image and disks.")
|
|
375
|
+
)
|
|
376
|
+
state = runtime(ctx)
|
|
377
|
+
selected_zone = zone or state.zone
|
|
378
|
+
selected_region = region_from_zone(selected_zone)
|
|
379
|
+
params = request(ctx, region_value=selected_region)
|
|
380
|
+
params.update(
|
|
381
|
+
compact(
|
|
382
|
+
{
|
|
383
|
+
"Zone": selected_zone,
|
|
384
|
+
"MachineTypes": gpu,
|
|
385
|
+
"InstanceType": "spot" if spot else "uhost",
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
legal = call(state, "DescribeAvailableCompShareInstanceTypes", params)
|
|
390
|
+
inventory: Dict[str, Dict[tuple, bool]] = {}
|
|
391
|
+
inventory_response: Dict[str, List[Dict[str, Any]]] = {}
|
|
392
|
+
if image is not None:
|
|
393
|
+
machine_types = {
|
|
394
|
+
machine.get("Name")
|
|
395
|
+
for machine in legal.get("AvailableInstanceTypes", [])
|
|
396
|
+
if machine.get("Name")
|
|
397
|
+
}
|
|
398
|
+
for machine_type in sorted(machine_types):
|
|
399
|
+
capacity_params = request(ctx, region_value=selected_region)
|
|
400
|
+
capacity_params.update(
|
|
401
|
+
{
|
|
402
|
+
"Zone": selected_zone,
|
|
403
|
+
"GpuType": machine_type,
|
|
404
|
+
"MachineType": "G",
|
|
405
|
+
"MinimalCpuPlatform": platform,
|
|
406
|
+
"CompShareImageId": image,
|
|
407
|
+
"ChargeType": charge or ("Spot" if spot else "Postpay"),
|
|
408
|
+
"Disks": [{"IsBoot": True, "Type": disk_type, "Size": disk_gib(disk)}],
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
capacity = call(state, "CheckCompShareResourceCapacity", capacity_params)
|
|
412
|
+
inventory_response[machine_type] = capacity.get("Specs", [])
|
|
413
|
+
inventory[machine_type] = {
|
|
414
|
+
(spec.get("Gpu"), spec.get("Cpu"), spec.get("Mem")): bool(
|
|
415
|
+
spec.get("ResourceEnough")
|
|
416
|
+
)
|
|
417
|
+
for spec in capacity.get("Specs", [])
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
response = dict(legal)
|
|
421
|
+
if image is not None:
|
|
422
|
+
response["Inventory"] = inventory_response
|
|
423
|
+
Renderer(state.json_output).data(
|
|
424
|
+
response,
|
|
425
|
+
rows=_search_rows(legal, inventory, available),
|
|
426
|
+
columns=(
|
|
427
|
+
("GpuType", "GPU"),
|
|
428
|
+
("GPU", "COUNT"),
|
|
429
|
+
("VRAM", "VRAM"),
|
|
430
|
+
("CPU", "CPU"),
|
|
431
|
+
("Memory", "MEMORY"),
|
|
432
|
+
("Zone", "ZONE"),
|
|
433
|
+
("Stock", "IN STOCK"),
|
|
434
|
+
("Platforms", "CPU PLATFORM"),
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@app.command("zones")
|
|
440
|
+
def zones(ctx: typer.Context) -> None:
|
|
441
|
+
"""List supported regions and availability zones."""
|
|
442
|
+
invoke(
|
|
443
|
+
runtime(ctx),
|
|
444
|
+
"DescribeCompShareSupportZone",
|
|
445
|
+
request(ctx),
|
|
446
|
+
list_key="ZoneInfo",
|
|
447
|
+
columns=(("Region", "REGION"), ("Zone", "ZONE"), ("Describe", "NAME")),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@app.command("families")
|
|
452
|
+
def families(ctx: typer.Context) -> None:
|
|
453
|
+
"""List GPU machine families."""
|
|
454
|
+
invoke(
|
|
455
|
+
runtime(ctx),
|
|
456
|
+
"DescribeCompShareMachineTypeFamilies",
|
|
457
|
+
request(ctx),
|
|
458
|
+
list_key="MachineTypes",
|
|
459
|
+
columns=(("Name", "NAME"), ("Description", "DESCRIPTION")),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@app.command("list")
|
|
464
|
+
def list_instances(
|
|
465
|
+
ctx: typer.Context,
|
|
466
|
+
ids: Optional[List[str]] = typer.Option(None, "--id", help="Instance ID; repeatable."),
|
|
467
|
+
region: Optional[str] = typer.Option(None, "--region", help="Filter by region."),
|
|
468
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Filter by availability zone."),
|
|
469
|
+
limit: int = typer.Option(20, min=1, max=100, help="Maximum number of results."),
|
|
470
|
+
offset: int = typer.Option(0, min=0, help="Number of results to skip."),
|
|
471
|
+
tag: Optional[str] = typer.Option(None, help="Filter by instance tag."),
|
|
472
|
+
vpc: Optional[str] = typer.Option(None, "--vpc", help="Filter by VPC ID."),
|
|
473
|
+
subnet: Optional[str] = typer.Option(None, "--subnet", help="Filter by subnet ID."),
|
|
474
|
+
disk: Optional[str] = typer.Option(None, "--disk", help="Filter hosts compatible with a disk."),
|
|
475
|
+
project_id: Optional[str] = typer.Option(
|
|
476
|
+
None,
|
|
477
|
+
"--project-id",
|
|
478
|
+
help="Project ID for this request.",
|
|
479
|
+
),
|
|
480
|
+
without_gpu: bool = typer.Option(False, "--without-gpu", help="List no-GPU instances."),
|
|
481
|
+
name: Optional[str] = typer.Option(None, "--name", help="Filter by instance name."),
|
|
482
|
+
status: Optional[str] = typer.Option(None, "--status", help="Filter by instance state."),
|
|
483
|
+
gpu: Optional[str] = typer.Option(None, "--gpu", help="Filter by GPU type."),
|
|
484
|
+
billing: Optional[str] = typer.Option(None, "--billing", help="Filter by billing type."),
|
|
485
|
+
) -> None:
|
|
486
|
+
"""List instances."""
|
|
487
|
+
state = runtime(ctx)
|
|
488
|
+
selected_region = region_from_zone(zone) if zone else region
|
|
489
|
+
params = request(ctx, region_value=selected_region)
|
|
490
|
+
params.update(
|
|
491
|
+
compact(
|
|
492
|
+
{
|
|
493
|
+
"Zone": zone,
|
|
494
|
+
"ProjectId": project_id,
|
|
495
|
+
"UHostIds": ids,
|
|
496
|
+
"Limit": limit,
|
|
497
|
+
"Offset": offset,
|
|
498
|
+
"Tag": tag,
|
|
499
|
+
"VPCId": vpc,
|
|
500
|
+
"SubnetId": subnet,
|
|
501
|
+
"UDiskIdForAttachment": disk,
|
|
502
|
+
"WithoutGpu": without_gpu if without_gpu else None,
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
regions = [selected_region] if selected_region else supported_regions(state)
|
|
507
|
+
response: Dict[str, Any] = {"UHostSet": [], "RegionSet": regions}
|
|
508
|
+
for current_region in regions:
|
|
509
|
+
current = call(
|
|
510
|
+
state,
|
|
511
|
+
"DescribeCompShareInstance",
|
|
512
|
+
{**params, "Region": current_region},
|
|
513
|
+
)
|
|
514
|
+
for host in current.get("UHostSet", []):
|
|
515
|
+
item = dict(host)
|
|
516
|
+
item.setdefault("Region", current_region)
|
|
517
|
+
response["UHostSet"].append(item)
|
|
518
|
+
response["TotalCount"] = len(response["UHostSet"])
|
|
519
|
+
filters = {
|
|
520
|
+
"Name": name.casefold() if name else None,
|
|
521
|
+
"State": status.casefold() if status else None,
|
|
522
|
+
"GpuType": gpu.casefold() if gpu else None,
|
|
523
|
+
"ChargeType": billing.casefold() if billing else None,
|
|
524
|
+
}
|
|
525
|
+
filtered = []
|
|
526
|
+
for host in response.get("UHostSet", []):
|
|
527
|
+
if all(
|
|
528
|
+
expected is None or expected in str(host.get(field, "")).casefold()
|
|
529
|
+
for field, expected in filters.items()
|
|
530
|
+
):
|
|
531
|
+
filtered.append(host)
|
|
532
|
+
result = dict(response)
|
|
533
|
+
result["UHostSet"] = filtered
|
|
534
|
+
result["FilteredCount"] = len(filtered)
|
|
535
|
+
Renderer(state.json_output).data(
|
|
536
|
+
result,
|
|
537
|
+
rows=_instance_rows(result),
|
|
538
|
+
columns=INSTANCE_COLUMNS,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@app.command("show")
|
|
543
|
+
def show(ctx: typer.Context, instance: str = typer.Argument(..., help="Instance ID.")) -> None:
|
|
544
|
+
"""Show full instance details."""
|
|
545
|
+
state = runtime(ctx)
|
|
546
|
+
region, zone, host = locate_instance(state, instance)
|
|
547
|
+
response = {"UHostSet": [host], "ResolvedRegion": region, "ResolvedZone": zone}
|
|
548
|
+
Renderer(state.json_output).details(
|
|
549
|
+
"Instance details",
|
|
550
|
+
[
|
|
551
|
+
("ID", host.get("UHostId")),
|
|
552
|
+
("NAME", host.get("Name")),
|
|
553
|
+
("STATE", host.get("State")),
|
|
554
|
+
("GPU", f"{host.get('GpuType', '-')} × {host.get('GPU', '-')}"),
|
|
555
|
+
("CPU", host.get("CPU")),
|
|
556
|
+
(
|
|
557
|
+
"MEMORY",
|
|
558
|
+
f"{host.get('Memory', 0) // 1024}GiB"
|
|
559
|
+
if isinstance(host.get("Memory"), int)
|
|
560
|
+
else host.get("Memory"),
|
|
561
|
+
),
|
|
562
|
+
("REGION", region),
|
|
563
|
+
("ZONE", host.get("Zone")),
|
|
564
|
+
("CHARGE", host.get("ChargeType")),
|
|
565
|
+
("IMAGE", host.get("CompShareImageId") or host.get("ImageId")),
|
|
566
|
+
("Password", host.get("Password")),
|
|
567
|
+
("SSH", host.get("SshLoginCommand")),
|
|
568
|
+
("DATA DISKS", host.get("DiskSet") or host.get("UDiskSet")),
|
|
569
|
+
],
|
|
570
|
+
response=response,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@app.command("create")
|
|
575
|
+
def create(
|
|
576
|
+
ctx: typer.Context,
|
|
577
|
+
gpu: Optional[str] = typer.Option(None, "--gpu", help="GPU type, for example 4090."),
|
|
578
|
+
count: Optional[int] = typer.Option(None, "--count", min=1, help="GPU count."),
|
|
579
|
+
cpu: Optional[int] = typer.Option(None, "--cpu", min=1, help="CPU core count."),
|
|
580
|
+
memory: Optional[str] = typer.Option(None, "--memory", help="Memory, for example 64GiB."),
|
|
581
|
+
image: Optional[str] = typer.Option(None, "--image", help="CompShare image ID."),
|
|
582
|
+
image_source: Optional[str] = typer.Option(
|
|
583
|
+
None,
|
|
584
|
+
"--image-source",
|
|
585
|
+
help="Image source: platform, custom, community or shared.",
|
|
586
|
+
),
|
|
587
|
+
zone: Optional[str] = typer.Option(
|
|
588
|
+
None,
|
|
589
|
+
"--zone",
|
|
590
|
+
help="Availability zone for this request.",
|
|
591
|
+
),
|
|
592
|
+
boot_disk: Optional[str] = typer.Option(None, "--disk", help="Boot disk size."),
|
|
593
|
+
boot_type: Optional[str] = typer.Option(None, "--disk-type", help="Boot disk type."),
|
|
594
|
+
data_disk: Optional[List[str]] = typer.Option(
|
|
595
|
+
None,
|
|
596
|
+
"--data-disk",
|
|
597
|
+
help="Data disk as SIZE[:TYPE]; repeatable.",
|
|
598
|
+
),
|
|
599
|
+
charge: Optional[str] = typer.Option(None, "--charge", help="Billing type."),
|
|
600
|
+
quantity: int = typer.Option(1, min=1, help="Billing duration for prepaid modes."),
|
|
601
|
+
name: Optional[str] = typer.Option(None, help="Instance name."),
|
|
602
|
+
platform: str = typer.Option("Auto", "--platform", help="Minimum CPU platform."),
|
|
603
|
+
remark: Optional[str] = typer.Option(None, help="Instance remark."),
|
|
604
|
+
firewall: Optional[str] = typer.Option(None, "--firewall", help="Security group ID."),
|
|
605
|
+
max_count: int = typer.Option(1, "--max-count", min=1, help="Number of instances."),
|
|
606
|
+
us3: bool = typer.Option(False, "--us3", help="Attach US3 during container creation."),
|
|
607
|
+
dry_run: bool = typer.Option(
|
|
608
|
+
False,
|
|
609
|
+
"--dry-run",
|
|
610
|
+
help="Validate and show the request without changing resources.",
|
|
611
|
+
),
|
|
612
|
+
wait: Optional[bool] = typer.Option(
|
|
613
|
+
None,
|
|
614
|
+
"--wait/--no-wait",
|
|
615
|
+
help="Wait for the operation to reach a stable state.",
|
|
616
|
+
),
|
|
617
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
618
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
619
|
+
) -> None:
|
|
620
|
+
"""Create instances interactively, or use explicit options for automation."""
|
|
621
|
+
state = runtime(ctx)
|
|
622
|
+
interactive = any(value is None for value in (gpu, count, cpu, memory, image))
|
|
623
|
+
if interactive and state.json_output:
|
|
624
|
+
raise UsageError(
|
|
625
|
+
tr(
|
|
626
|
+
"JSON mode cannot start the interactive wizard; pass --gpu, --count, --cpu, "
|
|
627
|
+
"--memory, and --image."
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
region, selected_zone = _create_location(state, zone, interactive=interactive)
|
|
632
|
+
resolved_gpu = _create_gpu(state, region, selected_zone, gpu)
|
|
633
|
+
resolved_image = _create_image(
|
|
634
|
+
state,
|
|
635
|
+
region,
|
|
636
|
+
selected_zone,
|
|
637
|
+
image,
|
|
638
|
+
image_source,
|
|
639
|
+
)
|
|
640
|
+
resolved_charge = _create_charge(charge) if interactive else charge or "Postpay"
|
|
641
|
+
resolved_boot_disk = boot_disk
|
|
642
|
+
resolved_boot_type = boot_type
|
|
643
|
+
if interactive:
|
|
644
|
+
resolved_boot_disk = resolved_boot_disk or typer.prompt(
|
|
645
|
+
tr("Boot disk size"),
|
|
646
|
+
default="100GiB",
|
|
647
|
+
)
|
|
648
|
+
resolved_boot_type = resolved_boot_type or typer.prompt(
|
|
649
|
+
tr("Boot disk type"),
|
|
650
|
+
default="CLOUD_SSD",
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
resolved_boot_disk = resolved_boot_disk or "100GiB"
|
|
654
|
+
resolved_boot_type = resolved_boot_type or "CLOUD_SSD"
|
|
655
|
+
|
|
656
|
+
disks = _disk_list(resolved_boot_disk, resolved_boot_type, data_disk)
|
|
657
|
+
common = {"Region": region, "Zone": selected_zone}
|
|
658
|
+
capacity_params = dict(common)
|
|
659
|
+
capacity_params.update(
|
|
660
|
+
{
|
|
661
|
+
"GpuType": resolved_gpu,
|
|
662
|
+
"MachineType": "G",
|
|
663
|
+
"MinimalCpuPlatform": platform,
|
|
664
|
+
"CompShareImageId": resolved_image,
|
|
665
|
+
"ChargeType": resolved_charge,
|
|
666
|
+
"Disks": disks,
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
capacity = call(state, "CheckCompShareResourceCapacity", capacity_params)
|
|
670
|
+
requested_memory = memory_mib(memory) // 1024 if memory is not None else None
|
|
671
|
+
matching = [
|
|
672
|
+
spec
|
|
673
|
+
for spec in capacity.get("Specs", [])
|
|
674
|
+
if spec.get("ResourceEnough")
|
|
675
|
+
and (count is None or spec.get("Gpu") == count)
|
|
676
|
+
and (cpu is None or spec.get("Cpu") == cpu)
|
|
677
|
+
and (requested_memory is None or spec.get("Mem") == requested_memory)
|
|
678
|
+
]
|
|
679
|
+
if not matching:
|
|
680
|
+
Renderer(state.json_output).error(
|
|
681
|
+
tr(
|
|
682
|
+
"No inventory is available for the selected GPU, CPU, memory, image, billing, "
|
|
683
|
+
"and disk combination."
|
|
684
|
+
),
|
|
685
|
+
details={"capacity": capacity},
|
|
686
|
+
)
|
|
687
|
+
raise typer.Exit(2)
|
|
688
|
+
|
|
689
|
+
selected_spec = matching[0]
|
|
690
|
+
if interactive and any(value is None for value in (count, cpu, memory)):
|
|
691
|
+
selected_spec = _choose(
|
|
692
|
+
"Available specification",
|
|
693
|
+
matching,
|
|
694
|
+
lambda spec: (
|
|
695
|
+
f"GPU ×{spec.get('Gpu')} · {spec.get('Cpu')} CPU · {spec.get('Mem')}GiB memory"
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
resolved_count = int(count if count is not None else selected_spec["Gpu"])
|
|
699
|
+
resolved_cpu = int(cpu if cpu is not None else selected_spec["Cpu"])
|
|
700
|
+
resolved_memory_gib = int(
|
|
701
|
+
requested_memory if requested_memory is not None else selected_spec["Mem"]
|
|
702
|
+
)
|
|
703
|
+
memory_mb = resolved_memory_gib * 1024
|
|
704
|
+
|
|
705
|
+
price_params = dict(common)
|
|
706
|
+
price_params.update(
|
|
707
|
+
{
|
|
708
|
+
"GpuType": resolved_gpu,
|
|
709
|
+
"Gpu": resolved_count,
|
|
710
|
+
"Cpu": resolved_cpu,
|
|
711
|
+
"Memory": memory_mb,
|
|
712
|
+
"ChargeType": resolved_charge,
|
|
713
|
+
"Disks": disks,
|
|
714
|
+
"CompShareImageId": resolved_image,
|
|
715
|
+
"Quantity": quantity,
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
price = call(state, "GetCompShareInstancePrice", price_params)
|
|
719
|
+
price_details = price.get("PriceDetails", [])
|
|
720
|
+
amount = None
|
|
721
|
+
if price_details:
|
|
722
|
+
values = [
|
|
723
|
+
price_details[0].get(key)
|
|
724
|
+
for key in ("Instance", "Disks", "SystemDisks", "CompShareImage")
|
|
725
|
+
]
|
|
726
|
+
numbers = [value for value in values if isinstance(value, (int, float))]
|
|
727
|
+
amount = sum(numbers) if numbers else None
|
|
728
|
+
price_text = "unknown" if amount is None else str(round(amount * max_count, 4))
|
|
729
|
+
create_params = dict(common)
|
|
730
|
+
create_params.update(
|
|
731
|
+
compact(
|
|
732
|
+
{
|
|
733
|
+
"GpuType": resolved_gpu,
|
|
734
|
+
"GPU": resolved_count,
|
|
735
|
+
"CPU": resolved_cpu,
|
|
736
|
+
"Memory": memory_mb,
|
|
737
|
+
"MachineType": "G",
|
|
738
|
+
"MinimalCpuPlatform": platform,
|
|
739
|
+
"CompShareImageId": resolved_image,
|
|
740
|
+
"Disks": disks,
|
|
741
|
+
"ChargeType": resolved_charge,
|
|
742
|
+
"Quantity": quantity,
|
|
743
|
+
"Name": name,
|
|
744
|
+
"Remark": remark,
|
|
745
|
+
"SecurityGroupId": firewall,
|
|
746
|
+
"MaxCount": max_count,
|
|
747
|
+
"EnableUS3": us3 if us3 else None,
|
|
748
|
+
}
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
selection = {
|
|
752
|
+
"Region": region,
|
|
753
|
+
"Zone": selected_zone,
|
|
754
|
+
"GpuType": resolved_gpu,
|
|
755
|
+
"GPU": resolved_count,
|
|
756
|
+
"CPU": resolved_cpu,
|
|
757
|
+
"Memory": memory_mb,
|
|
758
|
+
"CompShareImageId": resolved_image,
|
|
759
|
+
"Disks": disks,
|
|
760
|
+
"ChargeType": resolved_charge,
|
|
761
|
+
"MaxCount": max_count,
|
|
762
|
+
}
|
|
763
|
+
plan = {
|
|
764
|
+
"dry_run": dry_run,
|
|
765
|
+
"selection": selection,
|
|
766
|
+
"capacity": selected_spec,
|
|
767
|
+
"price": price,
|
|
768
|
+
"request": create_params,
|
|
769
|
+
}
|
|
770
|
+
if dry_run:
|
|
771
|
+
Renderer(state.json_output).details(
|
|
772
|
+
"Create plan",
|
|
773
|
+
[
|
|
774
|
+
("ZONE", selected_zone),
|
|
775
|
+
("GPU", f"{resolved_gpu} × {resolved_count}"),
|
|
776
|
+
("CPU", resolved_cpu),
|
|
777
|
+
("MEMORY", f"{resolved_memory_gib}GiB"),
|
|
778
|
+
("IMAGE", resolved_image),
|
|
779
|
+
("SYSTEM DISK", f"{resolved_boot_disk}:{resolved_boot_type}"),
|
|
780
|
+
("CHARGE", resolved_charge),
|
|
781
|
+
("COUNT", max_count),
|
|
782
|
+
("PRICE", price_text),
|
|
783
|
+
],
|
|
784
|
+
response=plan,
|
|
785
|
+
)
|
|
786
|
+
return
|
|
787
|
+
confirm_details(
|
|
788
|
+
state,
|
|
789
|
+
"Create plan",
|
|
790
|
+
[
|
|
791
|
+
("ZONE", selected_zone),
|
|
792
|
+
("GPU", f"{resolved_gpu} × {resolved_count}"),
|
|
793
|
+
("CPU", resolved_cpu),
|
|
794
|
+
("MEMORY", f"{resolved_memory_gib}GiB"),
|
|
795
|
+
("IMAGE", resolved_image),
|
|
796
|
+
("SYSTEM DISK", f"{resolved_boot_disk}:{resolved_boot_type}"),
|
|
797
|
+
("CHARGE", resolved_charge),
|
|
798
|
+
("COUNT", max_count),
|
|
799
|
+
("PRICE", price_text),
|
|
800
|
+
],
|
|
801
|
+
"Confirm this operation?",
|
|
802
|
+
yes,
|
|
803
|
+
)
|
|
804
|
+
created = call(state, "CreateCompShareInstance", create_params)
|
|
805
|
+
result = {
|
|
806
|
+
"selection": selection,
|
|
807
|
+
"capacity": selected_spec,
|
|
808
|
+
"price": price,
|
|
809
|
+
"instance": created,
|
|
810
|
+
}
|
|
811
|
+
ids = created.get("UHostIds") or created.get("UHostId") or []
|
|
812
|
+
if isinstance(ids, str):
|
|
813
|
+
ids = [ids]
|
|
814
|
+
if _wait_enabled(state, wait):
|
|
815
|
+
result["final"] = [
|
|
816
|
+
_wait_for_instance(state, item, region=region, timeout=timeout) for item in ids
|
|
817
|
+
]
|
|
818
|
+
Renderer(state.json_output).details(
|
|
819
|
+
"Operation completed",
|
|
820
|
+
[
|
|
821
|
+
("INSTANCE", ids),
|
|
822
|
+
("Password", created.get("Password")),
|
|
823
|
+
("ZONE", selected_zone),
|
|
824
|
+
("GPU", f"{resolved_gpu} × {resolved_count}"),
|
|
825
|
+
("PRICE", price_text),
|
|
826
|
+
],
|
|
827
|
+
response=result,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _lifecycle(
|
|
832
|
+
ctx: typer.Context,
|
|
833
|
+
action: str,
|
|
834
|
+
instance: str,
|
|
835
|
+
message: str,
|
|
836
|
+
*,
|
|
837
|
+
yes: bool,
|
|
838
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
839
|
+
wait: Optional[bool] = None,
|
|
840
|
+
timeout: int = 600,
|
|
841
|
+
desired: Optional[set[str]] = None,
|
|
842
|
+
absent: bool = False,
|
|
843
|
+
) -> None:
|
|
844
|
+
state = runtime(ctx)
|
|
845
|
+
region, zone, _ = locate_instance(state, instance)
|
|
846
|
+
confirm_details(
|
|
847
|
+
state,
|
|
848
|
+
"Operation plan",
|
|
849
|
+
[("INSTANCE", instance), ("ACTION", tr(message.rstrip("?")))],
|
|
850
|
+
"Confirm this operation?",
|
|
851
|
+
yes,
|
|
852
|
+
)
|
|
853
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
854
|
+
params["UHostId"] = instance
|
|
855
|
+
params.update(extra or {})
|
|
856
|
+
submitted = call(state, action, params)
|
|
857
|
+
result: Dict[str, Any] = {"operation": submitted}
|
|
858
|
+
if _wait_enabled(state, wait):
|
|
859
|
+
result["final"] = _wait_for_instance(
|
|
860
|
+
state,
|
|
861
|
+
instance,
|
|
862
|
+
region=region,
|
|
863
|
+
desired=desired,
|
|
864
|
+
absent=absent,
|
|
865
|
+
timeout=timeout,
|
|
866
|
+
)
|
|
867
|
+
Renderer(state.json_output).success(tr("Operation completed"), result)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
@app.command("start", help="Start an instance.")
|
|
871
|
+
def start(
|
|
872
|
+
ctx: typer.Context,
|
|
873
|
+
instance: str,
|
|
874
|
+
without_gpu: Optional[str] = typer.Option(
|
|
875
|
+
None, "--without-gpu", help="No-GPU specification: A or B."
|
|
876
|
+
),
|
|
877
|
+
wait: Optional[bool] = typer.Option(
|
|
878
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
879
|
+
),
|
|
880
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
881
|
+
) -> None:
|
|
882
|
+
state = runtime(ctx)
|
|
883
|
+
region, zone, _ = locate_instance(state, instance)
|
|
884
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
885
|
+
params.update(compact({"UHostId": instance, "WithoutGpuSpec": without_gpu}))
|
|
886
|
+
submitted = call(state, "StartCompShareInstance", params)
|
|
887
|
+
result: Dict[str, Any] = {"operation": submitted}
|
|
888
|
+
if _wait_enabled(state, wait):
|
|
889
|
+
result["final"] = _wait_for_instance(
|
|
890
|
+
state,
|
|
891
|
+
instance,
|
|
892
|
+
region=region,
|
|
893
|
+
desired={"Running"},
|
|
894
|
+
timeout=timeout,
|
|
895
|
+
)
|
|
896
|
+
Renderer(state.json_output).success(tr("Operation completed"), result)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@app.command("stop", help="Stop an instance.")
|
|
900
|
+
def stop(
|
|
901
|
+
ctx: typer.Context,
|
|
902
|
+
instance: str,
|
|
903
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
904
|
+
wait: Optional[bool] = typer.Option(
|
|
905
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
906
|
+
),
|
|
907
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
908
|
+
) -> None:
|
|
909
|
+
_lifecycle(
|
|
910
|
+
ctx,
|
|
911
|
+
"StopCompShareInstance",
|
|
912
|
+
instance,
|
|
913
|
+
"Stop instance",
|
|
914
|
+
yes=yes,
|
|
915
|
+
wait=wait,
|
|
916
|
+
timeout=timeout,
|
|
917
|
+
desired={"Stopped"},
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@app.command("reboot", help="Reboot an instance.")
|
|
922
|
+
def reboot(
|
|
923
|
+
ctx: typer.Context,
|
|
924
|
+
instance: str,
|
|
925
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
926
|
+
wait: Optional[bool] = typer.Option(
|
|
927
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
928
|
+
),
|
|
929
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
930
|
+
) -> None:
|
|
931
|
+
_lifecycle(
|
|
932
|
+
ctx,
|
|
933
|
+
"RebootCompShareInstance",
|
|
934
|
+
instance,
|
|
935
|
+
"Reboot instance",
|
|
936
|
+
yes=yes,
|
|
937
|
+
wait=wait,
|
|
938
|
+
timeout=timeout,
|
|
939
|
+
desired={"Running"},
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
@app.command("delete", help="Permanently delete an instance.")
|
|
944
|
+
def delete(
|
|
945
|
+
ctx: typer.Context,
|
|
946
|
+
instance: str,
|
|
947
|
+
release_disk: bool = typer.Option(
|
|
948
|
+
False, "--release-disk", help="Delete attached data disks with the instance."
|
|
949
|
+
),
|
|
950
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
951
|
+
wait: Optional[bool] = typer.Option(
|
|
952
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
953
|
+
),
|
|
954
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
955
|
+
) -> None:
|
|
956
|
+
_lifecycle(
|
|
957
|
+
ctx,
|
|
958
|
+
"TerminateCompShareInstance",
|
|
959
|
+
instance,
|
|
960
|
+
"Permanently delete instance and attached data disks"
|
|
961
|
+
if release_disk
|
|
962
|
+
else "Permanently delete instance",
|
|
963
|
+
yes=yes,
|
|
964
|
+
extra={"ReleaseUDisk": release_disk},
|
|
965
|
+
wait=wait,
|
|
966
|
+
timeout=timeout,
|
|
967
|
+
absent=True,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
@app.command("rename", help="Rename an instance.")
|
|
972
|
+
def rename(ctx: typer.Context, instance: str, name: str) -> None:
|
|
973
|
+
state = runtime(ctx)
|
|
974
|
+
region, zone, _ = locate_instance(state, instance)
|
|
975
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
976
|
+
params.update({"UHostId": instance, "Name": name})
|
|
977
|
+
invoke(
|
|
978
|
+
state,
|
|
979
|
+
"ModifyCompShareInstanceName",
|
|
980
|
+
params,
|
|
981
|
+
success=tr("Renamed {instance}", instance=instance),
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@app.command("password")
|
|
986
|
+
def password(
|
|
987
|
+
ctx: typer.Context,
|
|
988
|
+
instance: str,
|
|
989
|
+
value: Optional[str] = typer.Option(None, "--password", hidden=True),
|
|
990
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Reset an instance password."""
|
|
993
|
+
secret = value or typer.prompt(tr("New password"), hide_input=True, confirmation_prompt=True)
|
|
994
|
+
_lifecycle(
|
|
995
|
+
ctx,
|
|
996
|
+
"ResetCompShareInstancePassword",
|
|
997
|
+
instance,
|
|
998
|
+
"Reset instance password",
|
|
999
|
+
yes=yes,
|
|
1000
|
+
extra={"Password": encode_password(secret)},
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
@app.command("reinstall", help="Reinstall an instance from an image.")
|
|
1005
|
+
def reinstall(
|
|
1006
|
+
ctx: typer.Context,
|
|
1007
|
+
instance: str,
|
|
1008
|
+
image: str = typer.Option(..., "--image", help="Replacement image ID."),
|
|
1009
|
+
password: Optional[str] = typer.Option(None, "--password", hidden=True),
|
|
1010
|
+
coupon: Optional[str] = typer.Option(None, "--coupon", help="Coupon ID."),
|
|
1011
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
1012
|
+
wait: Optional[bool] = typer.Option(
|
|
1013
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
1014
|
+
),
|
|
1015
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
1016
|
+
) -> None:
|
|
1017
|
+
extra = compact(
|
|
1018
|
+
{
|
|
1019
|
+
"CompShareImageId": image,
|
|
1020
|
+
"Password": encode_password(password) if password else None,
|
|
1021
|
+
"CouponId": coupon,
|
|
1022
|
+
}
|
|
1023
|
+
)
|
|
1024
|
+
_lifecycle(
|
|
1025
|
+
ctx,
|
|
1026
|
+
"ReinstallCompShareInstance",
|
|
1027
|
+
instance,
|
|
1028
|
+
"Reinstall instance; all system disk data will be lost",
|
|
1029
|
+
yes=yes,
|
|
1030
|
+
extra=extra,
|
|
1031
|
+
wait=wait,
|
|
1032
|
+
timeout=timeout,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
@app.command("resize", help="Change instance CPU, memory, GPU or disk size.")
|
|
1037
|
+
def resize(
|
|
1038
|
+
ctx: typer.Context,
|
|
1039
|
+
instance: str,
|
|
1040
|
+
cpu: Optional[int] = typer.Option(None, min=1, help="Target CPU core count."),
|
|
1041
|
+
memory: Optional[str] = typer.Option(None, help="Target memory, for example 64GiB."),
|
|
1042
|
+
gpu: Optional[int] = typer.Option(None, min=0, help="Target GPU count."),
|
|
1043
|
+
without_gpu: Optional[str] = typer.Option(
|
|
1044
|
+
None, "--without-gpu", help="Target no-GPU specification: A or B."
|
|
1045
|
+
),
|
|
1046
|
+
disk: Optional[str] = typer.Option(None, "--disk", help="Disk ID to resize."),
|
|
1047
|
+
disk_size: Optional[str] = typer.Option(
|
|
1048
|
+
None, "--disk-size", help="Target disk size, for example 200GiB."
|
|
1049
|
+
),
|
|
1050
|
+
coupon: Optional[str] = typer.Option(None, "--coupon", help="Coupon ID."),
|
|
1051
|
+
dry_run: bool = typer.Option(
|
|
1052
|
+
False,
|
|
1053
|
+
"--dry-run",
|
|
1054
|
+
help="Validate and show the request without changing resources.",
|
|
1055
|
+
),
|
|
1056
|
+
wait: Optional[bool] = typer.Option(
|
|
1057
|
+
None, "--wait/--no-wait", help="Wait for the operation to reach a stable state."
|
|
1058
|
+
),
|
|
1059
|
+
timeout: int = typer.Option(600, "--timeout", min=1, help="Maximum wait time in seconds."),
|
|
1060
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
1061
|
+
) -> None:
|
|
1062
|
+
compute_values = (cpu, memory, gpu)
|
|
1063
|
+
compute_resize = any(value is not None for value in compute_values) or without_gpu is not None
|
|
1064
|
+
disk_resize = disk is not None or disk_size is not None
|
|
1065
|
+
if not compute_resize and not disk_resize:
|
|
1066
|
+
raise UsageError(tr("Specify a compute target or both --disk and --disk-size."))
|
|
1067
|
+
if (disk is None) != (disk_size is None):
|
|
1068
|
+
raise UsageError(tr("--disk and --disk-size must be used together."))
|
|
1069
|
+
if compute_resize and disk_resize:
|
|
1070
|
+
raise UsageError(tr("Compute resizing and disk resizing must be separate operations."))
|
|
1071
|
+
if without_gpu is not None and any(value is not None for value in compute_values):
|
|
1072
|
+
raise UsageError(tr("--without-gpu cannot be combined with --cpu, --memory, or --gpu."))
|
|
1073
|
+
if (
|
|
1074
|
+
without_gpu is None
|
|
1075
|
+
and compute_resize
|
|
1076
|
+
and not all(value is not None for value in compute_values)
|
|
1077
|
+
):
|
|
1078
|
+
raise UsageError(tr("Compute resizing requires --cpu, --memory, and --gpu together."))
|
|
1079
|
+
params = compact(
|
|
1080
|
+
{
|
|
1081
|
+
"Cpu": cpu,
|
|
1082
|
+
"Memory": memory_mib(memory) if memory else None,
|
|
1083
|
+
"Gpu": gpu,
|
|
1084
|
+
"WithoutGpuSpec": without_gpu,
|
|
1085
|
+
"DiskId": disk,
|
|
1086
|
+
"DiskSpace": disk_gib(disk_size) if disk_size else None,
|
|
1087
|
+
"CouponId": coupon,
|
|
1088
|
+
}
|
|
1089
|
+
)
|
|
1090
|
+
if dry_run:
|
|
1091
|
+
quote: Dict[str, Any] = {}
|
|
1092
|
+
if compute_resize and without_gpu is None:
|
|
1093
|
+
state = runtime(ctx)
|
|
1094
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1095
|
+
quote_params = request(
|
|
1096
|
+
ctx,
|
|
1097
|
+
zone=True,
|
|
1098
|
+
region_value=region,
|
|
1099
|
+
zone_value=zone,
|
|
1100
|
+
)
|
|
1101
|
+
quote_params.update(
|
|
1102
|
+
{"UHostId": instance, "CPU": cpu, "Memory": memory_mib(memory or ""), "GPU": gpu}
|
|
1103
|
+
)
|
|
1104
|
+
quote = call(runtime(ctx), "GetCompShareInstanceUpgradePrice", quote_params)
|
|
1105
|
+
Renderer(runtime(ctx).json_output).details(
|
|
1106
|
+
"Operation plan",
|
|
1107
|
+
[("INSTANCE", instance), ("ACTION", "Resize"), ("REQUEST", params), ("PRICE", quote)],
|
|
1108
|
+
response={"dry_run": True, "instance": instance, "request": params, "price": quote},
|
|
1109
|
+
)
|
|
1110
|
+
return
|
|
1111
|
+
_lifecycle(
|
|
1112
|
+
ctx,
|
|
1113
|
+
"ResizeCompShareInstance",
|
|
1114
|
+
instance,
|
|
1115
|
+
"Resize instance",
|
|
1116
|
+
yes=yes,
|
|
1117
|
+
extra=params,
|
|
1118
|
+
wait=wait,
|
|
1119
|
+
timeout=timeout,
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
@app.command("price", help="Query a new instance price.")
|
|
1124
|
+
def price(
|
|
1125
|
+
ctx: typer.Context,
|
|
1126
|
+
gpu: str = typer.Option(..., "--gpu", help="GPU type."),
|
|
1127
|
+
count: int = typer.Option(1, "--count", min=1, help="GPU count."),
|
|
1128
|
+
cpu: int = typer.Option(..., min=1, help="CPU core count."),
|
|
1129
|
+
memory: str = typer.Option(..., help="Memory, for example 64GiB."),
|
|
1130
|
+
charge: Optional[str] = typer.Option(None, help="Billing type."),
|
|
1131
|
+
disk: str = typer.Option("100GiB", help="Boot disk size."),
|
|
1132
|
+
disk_type: str = typer.Option("CLOUD_SSD", "--disk-type", help="Boot disk type."),
|
|
1133
|
+
volume: Optional[List[str]] = typer.Option(
|
|
1134
|
+
None,
|
|
1135
|
+
"--volume",
|
|
1136
|
+
help="Shared storage as SIZE[:TYPE]; repeatable.",
|
|
1137
|
+
),
|
|
1138
|
+
image: Optional[str] = typer.Option(None, help="Image ID for image pricing."),
|
|
1139
|
+
quantity: int = typer.Option(1, min=1, help="Billing duration for prepaid modes."),
|
|
1140
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Availability zone."),
|
|
1141
|
+
) -> None:
|
|
1142
|
+
selected_zone = zone or runtime(ctx).zone
|
|
1143
|
+
params = request(ctx, zone=True, zone_value=selected_zone)
|
|
1144
|
+
params.update(
|
|
1145
|
+
compact(
|
|
1146
|
+
{
|
|
1147
|
+
"GpuType": gpu,
|
|
1148
|
+
"Gpu": count,
|
|
1149
|
+
"Cpu": cpu,
|
|
1150
|
+
"Memory": memory_mib(memory),
|
|
1151
|
+
"ChargeType": charge,
|
|
1152
|
+
"Disks": [{"IsBoot": True, "Type": disk_type, "Size": disk_gib(disk)}],
|
|
1153
|
+
"Volumes": _volume_list(volume),
|
|
1154
|
+
"CompShareImageId": image,
|
|
1155
|
+
"Quantity": quantity,
|
|
1156
|
+
}
|
|
1157
|
+
)
|
|
1158
|
+
)
|
|
1159
|
+
invoke(
|
|
1160
|
+
runtime(ctx),
|
|
1161
|
+
"GetCompShareInstancePrice",
|
|
1162
|
+
params,
|
|
1163
|
+
list_key="PriceDetails",
|
|
1164
|
+
columns=(
|
|
1165
|
+
("ChargeType", "CHARGE"),
|
|
1166
|
+
("Instance", "INSTANCE"),
|
|
1167
|
+
("SystemDisks", "SYSTEM DISK"),
|
|
1168
|
+
("Disks", "DATA DISKS"),
|
|
1169
|
+
("CompShareImage", "IMAGE"),
|
|
1170
|
+
),
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
@app.command("upgrade-price", help="Query the price of an instance upgrade.", hidden=True)
|
|
1175
|
+
@app.command("resize-price", help="Query the price of an instance upgrade.")
|
|
1176
|
+
def upgrade_price(
|
|
1177
|
+
ctx: typer.Context,
|
|
1178
|
+
instance: str,
|
|
1179
|
+
cpu: Optional[int] = typer.Option(None, help="Target CPU core count."),
|
|
1180
|
+
memory: Optional[str] = typer.Option(None, help="Target memory, for example 64GiB."),
|
|
1181
|
+
gpu: Optional[int] = typer.Option(None, help="Target GPU count."),
|
|
1182
|
+
) -> None:
|
|
1183
|
+
if cpu is None and memory is None and gpu is None:
|
|
1184
|
+
raise UsageError(tr("Specify at least one of --cpu, --memory, or --gpu."))
|
|
1185
|
+
state = runtime(ctx)
|
|
1186
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1187
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
1188
|
+
params.update(
|
|
1189
|
+
compact(
|
|
1190
|
+
{
|
|
1191
|
+
"UHostId": instance,
|
|
1192
|
+
"CPU": cpu,
|
|
1193
|
+
"Memory": memory_mib(memory) if memory else None,
|
|
1194
|
+
"GPU": gpu,
|
|
1195
|
+
}
|
|
1196
|
+
)
|
|
1197
|
+
)
|
|
1198
|
+
invoke(state, "GetCompShareInstanceUpgradePrice", params)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
@app.command("billing", help="Query current instance pricing.")
|
|
1202
|
+
def billing(
|
|
1203
|
+
ctx: typer.Context,
|
|
1204
|
+
gpu: str = typer.Option(..., "--gpu", help="GPU type."),
|
|
1205
|
+
count: int = typer.Option(1, "--count", help="GPU count."),
|
|
1206
|
+
cpu: int = typer.Option(..., help="CPU core count."),
|
|
1207
|
+
memory: str = typer.Option(..., help="Memory, for example 64GiB."),
|
|
1208
|
+
charge: Optional[str] = typer.Option(None, help="Billing type."),
|
|
1209
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Availability zone."),
|
|
1210
|
+
) -> None:
|
|
1211
|
+
selected_zone = zone or runtime(ctx).zone
|
|
1212
|
+
params = request(ctx, zone=True, zone_value=selected_zone)
|
|
1213
|
+
params.update(
|
|
1214
|
+
compact(
|
|
1215
|
+
{
|
|
1216
|
+
"GpuType": gpu,
|
|
1217
|
+
"GPU": count,
|
|
1218
|
+
"CPU": cpu,
|
|
1219
|
+
"Memory": memory_mib(memory),
|
|
1220
|
+
"ChargeType": charge,
|
|
1221
|
+
}
|
|
1222
|
+
)
|
|
1223
|
+
)
|
|
1224
|
+
invoke(
|
|
1225
|
+
runtime(ctx),
|
|
1226
|
+
"GetCompShareInstanceUserPrice",
|
|
1227
|
+
params,
|
|
1228
|
+
list_key="PriceDetails",
|
|
1229
|
+
columns=(
|
|
1230
|
+
("ChargeType", "CHARGE"),
|
|
1231
|
+
("Instance", "INSTANCE"),
|
|
1232
|
+
("SystemDisks", "SYSTEM DISK"),
|
|
1233
|
+
("Disks", "DATA DISKS"),
|
|
1234
|
+
("CompShareImage", "IMAGE"),
|
|
1235
|
+
),
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
@app.command("refund", help="Query instance refund amounts.")
|
|
1240
|
+
def refund(ctx: typer.Context, instances: List[str] = typer.Argument(...)) -> None:
|
|
1241
|
+
state = runtime(ctx)
|
|
1242
|
+
groups: Dict[Tuple[str, str], List[str]] = {}
|
|
1243
|
+
for instance in instances:
|
|
1244
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1245
|
+
groups.setdefault((region, zone), []).append(instance)
|
|
1246
|
+
response: Dict[str, Any] = {"RefundPriceSet": []}
|
|
1247
|
+
for (region, zone), ids in groups.items():
|
|
1248
|
+
current = call(
|
|
1249
|
+
state,
|
|
1250
|
+
"GetCompShareRefundPrice",
|
|
1251
|
+
{"Region": region, "Zone": zone, "UHostIds": ids},
|
|
1252
|
+
)
|
|
1253
|
+
response["RefundPriceSet"].extend(current.get("RefundPriceSet", []))
|
|
1254
|
+
Renderer(state.json_output).data(
|
|
1255
|
+
response,
|
|
1256
|
+
rows=response["RefundPriceSet"],
|
|
1257
|
+
columns=(
|
|
1258
|
+
("UHostId", "INSTANCE"),
|
|
1259
|
+
("Code", "CODE"),
|
|
1260
|
+
("RefundPrice", "REFUND"),
|
|
1261
|
+
("Message", "MESSAGE"),
|
|
1262
|
+
),
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
@app.command(
|
|
1267
|
+
"monitor",
|
|
1268
|
+
help="Get instance monitoring data (currently unavailable in production).",
|
|
1269
|
+
)
|
|
1270
|
+
def monitor(
|
|
1271
|
+
ctx: typer.Context,
|
|
1272
|
+
instances: Optional[List[str]] = typer.Argument(None),
|
|
1273
|
+
region: Optional[str] = typer.Option(None, "--region", help="Region for this request."),
|
|
1274
|
+
) -> None:
|
|
1275
|
+
state = runtime(ctx)
|
|
1276
|
+
if not region and instances:
|
|
1277
|
+
region, _, _ = locate_instance(state, instances[0])
|
|
1278
|
+
params = request(ctx, region_value=region)
|
|
1279
|
+
params.update(compact({"UHostIds": instances}))
|
|
1280
|
+
invoke(state, "GetCompShareInstanceMonitor", params)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
@app.command("charge", help="Change an instance billing type.")
|
|
1284
|
+
def charge(
|
|
1285
|
+
ctx: typer.Context,
|
|
1286
|
+
instance: str,
|
|
1287
|
+
destination: str = typer.Option(..., "--to", help="Month, Day, Dynamic or Postpay."),
|
|
1288
|
+
dry_run: bool = typer.Option(
|
|
1289
|
+
False,
|
|
1290
|
+
"--dry-run",
|
|
1291
|
+
help="Validate and show the request without changing resources.",
|
|
1292
|
+
),
|
|
1293
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
1294
|
+
) -> None:
|
|
1295
|
+
if dry_run:
|
|
1296
|
+
Renderer(runtime(ctx).json_output).details(
|
|
1297
|
+
"Operation plan",
|
|
1298
|
+
[("INSTANCE", instance), ("CHARGE", destination)],
|
|
1299
|
+
response={"dry_run": True, "instance": instance, "destination": destination},
|
|
1300
|
+
)
|
|
1301
|
+
return
|
|
1302
|
+
_lifecycle(
|
|
1303
|
+
ctx,
|
|
1304
|
+
"SwitchChargeType",
|
|
1305
|
+
instance,
|
|
1306
|
+
"Change instance billing type",
|
|
1307
|
+
yes=yes,
|
|
1308
|
+
extra={"DestChargeType": destination},
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
@app.command("network", help="Check network accelerator status.")
|
|
1313
|
+
def network(
|
|
1314
|
+
ctx: typer.Context,
|
|
1315
|
+
region: Optional[str] = typer.Option(None, "--region", help="Region for this request."),
|
|
1316
|
+
) -> None:
|
|
1317
|
+
invoke(
|
|
1318
|
+
runtime(ctx),
|
|
1319
|
+
"CheckCompShareNetOptimizer",
|
|
1320
|
+
request(ctx, region_value=region),
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
@app.command("models", help="List models in the model repository.")
|
|
1325
|
+
def models(
|
|
1326
|
+
ctx: typer.Context,
|
|
1327
|
+
name: Optional[str] = typer.Option(None, help="Filter by model name."),
|
|
1328
|
+
tags: Optional[str] = typer.Option(None, help="Filter by model tags."),
|
|
1329
|
+
region: Optional[str] = typer.Option(None, "--region", help="Region for this request."),
|
|
1330
|
+
) -> None:
|
|
1331
|
+
params = request(ctx, region_value=region)
|
|
1332
|
+
params.update(compact({"name": name, "tags": tags}))
|
|
1333
|
+
invoke(
|
|
1334
|
+
runtime(ctx),
|
|
1335
|
+
"DescribeModelRepositoryModels",
|
|
1336
|
+
params,
|
|
1337
|
+
list_key="Models",
|
|
1338
|
+
columns=(
|
|
1339
|
+
("Name", "NAME"),
|
|
1340
|
+
("Path", "PATH"),
|
|
1341
|
+
("Tag", "TAG"),
|
|
1342
|
+
("Size", "SIZE"),
|
|
1343
|
+
("CreateTime", "CREATED"),
|
|
1344
|
+
),
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
@ports_app.command("list", help="List supported software ports.")
|
|
1349
|
+
def list_ports(
|
|
1350
|
+
ctx: typer.Context,
|
|
1351
|
+
region: Optional[str] = typer.Option(None, "--region", help="Region for this request."),
|
|
1352
|
+
) -> None:
|
|
1353
|
+
invoke(
|
|
1354
|
+
runtime(ctx),
|
|
1355
|
+
"DescribeCompShareSoftwarePort",
|
|
1356
|
+
request(ctx, region_value=region),
|
|
1357
|
+
list_key="SoftwarePort",
|
|
1358
|
+
columns=(("Software", "SOFTWARE"), ("Port", "PORT")),
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
@ports_app.command("update", help="Replace an instance's container port mappings.")
|
|
1363
|
+
def update_ports(
|
|
1364
|
+
ctx: typer.Context,
|
|
1365
|
+
instance: str,
|
|
1366
|
+
http: Optional[List[int]] = typer.Option(
|
|
1367
|
+
None, "--http", help="Complete HTTP port list; repeatable."
|
|
1368
|
+
),
|
|
1369
|
+
tcp: Optional[List[int]] = typer.Option(
|
|
1370
|
+
None, "--tcp", help="Complete TCP port list; repeatable."
|
|
1371
|
+
),
|
|
1372
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
1373
|
+
) -> None:
|
|
1374
|
+
if http is None and tcp is None:
|
|
1375
|
+
raise UsageError(tr("Specify at least one --http or --tcp port."))
|
|
1376
|
+
confirm(tr("Replace port mappings for {instance}?", instance=instance), yes)
|
|
1377
|
+
state = runtime(ctx)
|
|
1378
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1379
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
1380
|
+
params.update(compact({"UHostId": instance, "HttpPorts": http, "TcpPorts": tcp}))
|
|
1381
|
+
invoke(
|
|
1382
|
+
state,
|
|
1383
|
+
"UpdateCompShareInstancePorts",
|
|
1384
|
+
params,
|
|
1385
|
+
success=tr("Updated port mappings"),
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
@schedule_app.command("set", help="Schedule an instance shutdown.")
|
|
1390
|
+
def set_schedule(
|
|
1391
|
+
ctx: typer.Context,
|
|
1392
|
+
instance: str,
|
|
1393
|
+
at: str = typer.Option(..., "--at", help="Unix timestamp, ISO 8601, or relative time."),
|
|
1394
|
+
project_id: Optional[str] = typer.Option(
|
|
1395
|
+
None, "--project-id", help="Override the automatically detected project ID."
|
|
1396
|
+
),
|
|
1397
|
+
) -> None:
|
|
1398
|
+
stop_time = timestamp(at)
|
|
1399
|
+
if stop_time < int(time.time()) + 300:
|
|
1400
|
+
raise UsageError(tr("Scheduled shutdown must be at least five minutes from now."))
|
|
1401
|
+
state = runtime(ctx)
|
|
1402
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1403
|
+
params = request(
|
|
1404
|
+
ctx,
|
|
1405
|
+
zone=True,
|
|
1406
|
+
project_id=_project_id(state, project_id),
|
|
1407
|
+
region_value=region,
|
|
1408
|
+
zone_value=zone,
|
|
1409
|
+
)
|
|
1410
|
+
params.update({"UHostId": instance, "SchedulerStopTime": stop_time})
|
|
1411
|
+
invoke(state, "UpdateCompShareStopScheduler", params, success=tr("Scheduled shutdown"))
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
@schedule_app.command("cancel", help="Cancel an instance scheduled shutdown.")
|
|
1415
|
+
def cancel_schedule(
|
|
1416
|
+
ctx: typer.Context,
|
|
1417
|
+
instance: str,
|
|
1418
|
+
project_id: Optional[str] = typer.Option(
|
|
1419
|
+
None, "--project-id", help="Override the automatically detected project ID."
|
|
1420
|
+
),
|
|
1421
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
1422
|
+
) -> None:
|
|
1423
|
+
confirm(tr("Cancel scheduled shutdown for {instance}?", instance=instance), yes)
|
|
1424
|
+
state = runtime(ctx)
|
|
1425
|
+
region, _, _ = locate_instance(state, instance)
|
|
1426
|
+
params = request(
|
|
1427
|
+
ctx,
|
|
1428
|
+
project_id=_project_id(state, project_id),
|
|
1429
|
+
region_value=region,
|
|
1430
|
+
)
|
|
1431
|
+
params["UHostId"] = instance
|
|
1432
|
+
invoke(state, "DeleteCompShareStopScheduler", params, success=tr("Cancelled shutdown"))
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
@software_app.command("list", help="List supported instance software.")
|
|
1436
|
+
def list_software(
|
|
1437
|
+
ctx: typer.Context,
|
|
1438
|
+
region: Optional[str] = typer.Option(None, "--region", help="Region for this request."),
|
|
1439
|
+
) -> None:
|
|
1440
|
+
invoke(
|
|
1441
|
+
runtime(ctx),
|
|
1442
|
+
"DescribeCompShareSoftwarePort",
|
|
1443
|
+
request(ctx, region_value=region),
|
|
1444
|
+
list_key="SoftwarePort",
|
|
1445
|
+
columns=(("Software", "SOFTWARE"), ("Port", "PORT")),
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
@software_app.command(
|
|
1450
|
+
"url",
|
|
1451
|
+
help="Get an instance software access URL (currently unavailable in production).",
|
|
1452
|
+
)
|
|
1453
|
+
def software_url(ctx: typer.Context, instance: str, software: str) -> None:
|
|
1454
|
+
state = runtime(ctx)
|
|
1455
|
+
region, zone, _ = locate_instance(state, instance)
|
|
1456
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
1457
|
+
params.update({"UHostId": instance, "Software": software})
|
|
1458
|
+
invoke(state, "GetSoftwareURL", params)
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
@app.command("ssh", help="Open or print an instance SSH command.")
|
|
1462
|
+
def ssh(
|
|
1463
|
+
ctx: typer.Context,
|
|
1464
|
+
instance: str,
|
|
1465
|
+
print_only: bool = typer.Option(False, "--print", help="Print instead of executing SSH."),
|
|
1466
|
+
) -> None:
|
|
1467
|
+
state = runtime(ctx)
|
|
1468
|
+
region, zone, host = locate_instance(state, instance)
|
|
1469
|
+
command = host.get("SshLoginCommand")
|
|
1470
|
+
password = host.get("Password")
|
|
1471
|
+
if not command:
|
|
1472
|
+
raise UsageError(tr("Instance {instance} has no SSH login command.", instance=instance))
|
|
1473
|
+
if print_only or state.json_output:
|
|
1474
|
+
Renderer(state.json_output).data(
|
|
1475
|
+
{
|
|
1476
|
+
"instance": instance,
|
|
1477
|
+
"command": command,
|
|
1478
|
+
"password": password,
|
|
1479
|
+
}
|
|
1480
|
+
)
|
|
1481
|
+
return
|
|
1482
|
+
if password:
|
|
1483
|
+
typer.echo(f"Password: {password}")
|
|
1484
|
+
else:
|
|
1485
|
+
typer.echo(
|
|
1486
|
+
tr(
|
|
1487
|
+
"The API did not return a password. Run `compshare instance password {instance}` "
|
|
1488
|
+
"to set one.",
|
|
1489
|
+
instance=instance,
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
argv = shlex.split(command)
|
|
1493
|
+
raise typer.Exit(subprocess.call(argv))
|