compshare-cli 0.3.6__tar.gz → 0.4.1__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.
- {compshare_cli-0.3.6/src/compshare_cli.egg-info → compshare_cli-0.4.1}/PKG-INFO +15 -3
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/README.md +13 -1
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/pyproject.toml +1 -1
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/__init__.py +1 -1
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/actions.py +17 -2
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/cli.py +15 -9
- compshare_cli-0.4.1/src/compshare_cli/commands/bandwidth.py +452 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/common.py +6 -1
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/instance.py +7 -19
- compshare_cli-0.4.1/src/compshare_cli/commands/minimax.py +428 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/i18n.py +118 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/location.py +29 -2
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/sdk.py +1 -1
- {compshare_cli-0.3.6 → compshare_cli-0.4.1/src/compshare_cli.egg-info}/PKG-INFO +15 -3
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli.egg-info/SOURCES.txt +4 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_actions.py +5 -3
- compshare_cli-0.4.1/tests/test_bandwidth.py +273 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_cli.py +4 -2
- compshare_cli-0.4.1/tests/test_minimax.py +297 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_sdk.py +15 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/LICENSE +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/setup.cfg +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/__main__.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/api.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/__init__.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/ask.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/doctor.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/feedback.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/image.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/storage.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/commands/team.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/config.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/errors.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/insights.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/instance_templates.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/output.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/parsing.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/qa.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/remote_jobs.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/runtime.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/ssh.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli/ssh_cache.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli.egg-info/entry_points.txt +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli.egg-info/requires.txt +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/src/compshare_cli.egg-info/top_level.txt +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_api.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_config.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_feedback.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_instance_templates.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_location.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_output.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_parsing.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_qa.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_release_version.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_remote_jobs.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_ssh.py +0 -0
- {compshare_cli-0.3.6 → compshare_cli-0.4.1}/tests/test_ssh_cache.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compshare-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Command-line client for CompShare GPU compute
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Command-line client for CompShare GPU compute and MiniMax H3
|
|
5
5
|
Author: CompShare
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/BennielAllan/compshare-cli
|
|
@@ -25,12 +25,14 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
# CompShare CLI
|
|
27
27
|
|
|
28
|
-
在终端管理优云智算 GPU 实例、镜像、云盘、US3
|
|
28
|
+
在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载、独享带宽、团队资源和 MiniMax H3 视频任务。
|
|
29
29
|
|
|
30
30
|
- 交互式创建实例,实时查询规格、库存和价格
|
|
31
31
|
- 支持批量生命周期操作、等待状态和跨地域查询
|
|
32
32
|
- 支持可断线恢复的远程任务、状态查询和增量日志读取
|
|
33
33
|
- 支持社区/自定义镜像、云盘、团队额度与账单
|
|
34
|
+
- 支持独享带宽询价、购买、改配、退款预估及实例 EIP 切换
|
|
35
|
+
- 支持创建、查询和取消 MiniMax H3 视频任务,并查看积分套餐包与余额
|
|
34
36
|
- 默认中文帮助,可切换英文;所有命令支持 JSON 输出
|
|
35
37
|
- 可通过 `compshare ask` 查询产品使用和计费知识
|
|
36
38
|
- 可通过 `compshare feedback` 反馈 CLI 问题和建议
|
|
@@ -61,6 +63,9 @@ compshare --profile production instance list
|
|
|
61
63
|
```bash
|
|
62
64
|
export COMPSHARE_PUBLIC_KEY='...'
|
|
63
65
|
export COMPSHARE_PRIVATE_KEY='...'
|
|
66
|
+
|
|
67
|
+
# MiniMax H3 使用独立的模型 API Key
|
|
68
|
+
export COMPSHARE_MINIMAX_API_KEY='sk-ml-...'
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
|
|
@@ -77,6 +82,11 @@ compshare instance list --all
|
|
|
77
82
|
# 连接实例
|
|
78
83
|
compshare instance ssh INSTANCE_ID
|
|
79
84
|
|
|
85
|
+
# MiniMax H3:先预演,再创建
|
|
86
|
+
compshare --json minimax create '一只猫在海边奔跑' --dry-run
|
|
87
|
+
compshare --json minimax create '一只猫在海边奔跑' --yes
|
|
88
|
+
compshare --json minimax points
|
|
89
|
+
|
|
80
90
|
# 查看帮助
|
|
81
91
|
compshare -h
|
|
82
92
|
compshare instance -h
|
|
@@ -92,6 +102,8 @@ compshare ask 产品使用和计费问答
|
|
|
92
102
|
compshare instance GPU 实例、规格、库存、价格和生命周期
|
|
93
103
|
compshare image 平台、自定义、社区及共享镜像
|
|
94
104
|
compshare storage 云盘和 US3 挂载
|
|
105
|
+
compshare bandwidth 独享带宽和实例 EIP 带宽归属
|
|
106
|
+
compshare minimax MiniMax H3 视频任务和积分
|
|
95
107
|
compshare team 团队、邀请、成员额度、账单和审计
|
|
96
108
|
```
|
|
97
109
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# CompShare CLI
|
|
2
2
|
|
|
3
|
-
在终端管理优云智算 GPU 实例、镜像、云盘、US3
|
|
3
|
+
在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载、独享带宽、团队资源和 MiniMax H3 视频任务。
|
|
4
4
|
|
|
5
5
|
- 交互式创建实例,实时查询规格、库存和价格
|
|
6
6
|
- 支持批量生命周期操作、等待状态和跨地域查询
|
|
7
7
|
- 支持可断线恢复的远程任务、状态查询和增量日志读取
|
|
8
8
|
- 支持社区/自定义镜像、云盘、团队额度与账单
|
|
9
|
+
- 支持独享带宽询价、购买、改配、退款预估及实例 EIP 切换
|
|
10
|
+
- 支持创建、查询和取消 MiniMax H3 视频任务,并查看积分套餐包与余额
|
|
9
11
|
- 默认中文帮助,可切换英文;所有命令支持 JSON 输出
|
|
10
12
|
- 可通过 `compshare ask` 查询产品使用和计费知识
|
|
11
13
|
- 可通过 `compshare feedback` 反馈 CLI 问题和建议
|
|
@@ -36,6 +38,9 @@ compshare --profile production instance list
|
|
|
36
38
|
```bash
|
|
37
39
|
export COMPSHARE_PUBLIC_KEY='...'
|
|
38
40
|
export COMPSHARE_PRIVATE_KEY='...'
|
|
41
|
+
|
|
42
|
+
# MiniMax H3 使用独立的模型 API Key
|
|
43
|
+
export COMPSHARE_MINIMAX_API_KEY='sk-ml-...'
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
|
|
@@ -52,6 +57,11 @@ compshare instance list --all
|
|
|
52
57
|
# 连接实例
|
|
53
58
|
compshare instance ssh INSTANCE_ID
|
|
54
59
|
|
|
60
|
+
# MiniMax H3:先预演,再创建
|
|
61
|
+
compshare --json minimax create '一只猫在海边奔跑' --dry-run
|
|
62
|
+
compshare --json minimax create '一只猫在海边奔跑' --yes
|
|
63
|
+
compshare --json minimax points
|
|
64
|
+
|
|
55
65
|
# 查看帮助
|
|
56
66
|
compshare -h
|
|
57
67
|
compshare instance -h
|
|
@@ -67,6 +77,8 @@ compshare ask 产品使用和计费问答
|
|
|
67
77
|
compshare instance GPU 实例、规格、库存、价格和生命周期
|
|
68
78
|
compshare image 平台、自定义、社区及共享镜像
|
|
69
79
|
compshare storage 云盘和 US3 挂载
|
|
80
|
+
compshare bandwidth 独享带宽和实例 EIP 带宽归属
|
|
81
|
+
compshare minimax MiniMax H3 视频任务和积分
|
|
70
82
|
compshare team 团队、邀请、成员额度、账单和审计
|
|
71
83
|
```
|
|
72
84
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compshare-cli"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "Command-line client for CompShare GPU compute"
|
|
8
|
+
description = "Command-line client for CompShare GPU compute and MiniMax H3"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Public CompShare API actions covered by the
|
|
1
|
+
"""Public CompShare API actions covered by the CLI."""
|
|
2
2
|
|
|
3
3
|
INSTANCE_ACTIONS = frozenset(
|
|
4
4
|
{
|
|
@@ -88,7 +88,22 @@ TEAM_ACTIONS = frozenset(
|
|
|
88
88
|
}
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
BANDWIDTH_ACTIONS = frozenset(
|
|
92
|
+
{
|
|
93
|
+
"CreateCompShareShareBandwidth",
|
|
94
|
+
"DeleteCompShareShareBandwidth",
|
|
95
|
+
"DescribeCompShareShareBandwidth",
|
|
96
|
+
"GetCompShareShareBandwidthPrice",
|
|
97
|
+
"GetCompShareShareBandwidthRefundPrice",
|
|
98
|
+
"GetCompShareShareBandwidthUpgradePrice",
|
|
99
|
+
"ModifyCompShareShareBandwidth",
|
|
100
|
+
"SwitchCompShareEIPShareBandwidth",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
PUBLIC_ACTIONS = (
|
|
105
|
+
INSTANCE_ACTIONS | IMAGE_ACTIONS | STORAGE_ACTIONS | TEAM_ACTIONS | BANDWIDTH_ACTIONS
|
|
106
|
+
)
|
|
92
107
|
|
|
93
108
|
# No unavailable API actions are currently exposed as placeholders.
|
|
94
109
|
COMING_SOON_ACTIONS = frozenset()
|
|
@@ -11,9 +11,9 @@ from typer.main import get_command
|
|
|
11
11
|
|
|
12
12
|
from compshare_cli import __version__
|
|
13
13
|
from compshare_cli.commands import ask as ask_command
|
|
14
|
+
from compshare_cli.commands import bandwidth, image, instance, minimax, storage, team
|
|
14
15
|
from compshare_cli.commands import doctor as doctor_command
|
|
15
16
|
from compshare_cli.commands import feedback as feedback_command
|
|
16
|
-
from compshare_cli.commands import image, instance, storage, team
|
|
17
17
|
from compshare_cli.commands.common import confirm
|
|
18
18
|
from compshare_cli.config import DEFAULT_PROFILE, ConfigStore, Profile
|
|
19
19
|
from compshare_cli.errors import CLIError, ConfigError, UsageError
|
|
@@ -26,6 +26,10 @@ _TYPER_CLICK = getattr(typer_core, "_click", click)
|
|
|
26
26
|
_TYPER_CLICK_EXCEPTIONS = getattr(_TYPER_CLICK, "exceptions", _TYPER_CLICK)
|
|
27
27
|
_CLICK_EXCEPTIONS = (click.ClickException, _TYPER_CLICK_EXCEPTIONS.ClickException)
|
|
28
28
|
_ABORT_EXCEPTIONS = (click.Abort, _TYPER_CLICK_EXCEPTIONS.Abort)
|
|
29
|
+
_GROUP_TYPES = (click.Group, typer_core.TyperGroup)
|
|
30
|
+
_OPTION_TYPES = (click.Option, typer_core.TyperOption)
|
|
31
|
+
_ARGUMENT_TYPES = (click.Argument, typer_core.TyperArgument)
|
|
32
|
+
_CLICK_CONTEXT = getattr(_TYPER_CLICK, "Context", click.Context)
|
|
29
33
|
|
|
30
34
|
completion_init()
|
|
31
35
|
|
|
@@ -42,7 +46,7 @@ class RootGroup(typer_core.TyperGroup):
|
|
|
42
46
|
|
|
43
47
|
app = typer.Typer(
|
|
44
48
|
name="compshare",
|
|
45
|
-
help="Manage CompShare GPU compute from the terminal.",
|
|
49
|
+
help="Manage CompShare GPU compute and MiniMax H3 from the terminal.",
|
|
46
50
|
cls=RootGroup,
|
|
47
51
|
no_args_is_help=True,
|
|
48
52
|
add_completion=False,
|
|
@@ -51,6 +55,8 @@ app = typer.Typer(
|
|
|
51
55
|
app.add_typer(instance.app, name="instance")
|
|
52
56
|
app.add_typer(image.app, name="image")
|
|
53
57
|
app.add_typer(storage.app, name="storage")
|
|
58
|
+
app.add_typer(bandwidth.app, name="bandwidth")
|
|
59
|
+
app.add_typer(minimax.app, name="minimax")
|
|
54
60
|
app.add_typer(team.app, name="team")
|
|
55
61
|
config_app = typer.Typer(
|
|
56
62
|
help="Manage credential profiles.",
|
|
@@ -391,7 +397,7 @@ def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
|
|
|
391
397
|
|
|
392
398
|
path: List[str] = []
|
|
393
399
|
current = command
|
|
394
|
-
while remaining and isinstance(current,
|
|
400
|
+
while remaining and isinstance(current, _GROUP_TYPES):
|
|
395
401
|
name = remaining.pop(0)
|
|
396
402
|
child = current.commands.get(name)
|
|
397
403
|
if child is None:
|
|
@@ -429,13 +435,13 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
|
|
|
429
435
|
"--show-sensitive",
|
|
430
436
|
}:
|
|
431
437
|
continue
|
|
432
|
-
if isinstance(current,
|
|
438
|
+
if isinstance(current, _GROUP_TYPES) and token in current.commands:
|
|
433
439
|
current = current.commands[token]
|
|
434
440
|
path.append(token)
|
|
435
441
|
|
|
436
442
|
parameters: List[Dict[str, Any]] = []
|
|
437
443
|
for parameter in current.params:
|
|
438
|
-
if isinstance(parameter,
|
|
444
|
+
if isinstance(parameter, _OPTION_TYPES):
|
|
439
445
|
parameters.append(
|
|
440
446
|
{
|
|
441
447
|
"kind": "option",
|
|
@@ -446,7 +452,7 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
|
|
|
446
452
|
"help": parameter.help,
|
|
447
453
|
}
|
|
448
454
|
)
|
|
449
|
-
elif isinstance(parameter,
|
|
455
|
+
elif isinstance(parameter, _ARGUMENT_TYPES):
|
|
450
456
|
parameters.append(
|
|
451
457
|
{
|
|
452
458
|
"kind": "argument",
|
|
@@ -456,11 +462,11 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
|
|
|
456
462
|
}
|
|
457
463
|
)
|
|
458
464
|
commands = []
|
|
459
|
-
if isinstance(current,
|
|
465
|
+
if isinstance(current, _GROUP_TYPES):
|
|
460
466
|
commands = [
|
|
461
467
|
{"name": name, "help": child.get_short_help_str()}
|
|
462
|
-
for name in current.list_commands(
|
|
463
|
-
if (child := current.get_command(
|
|
468
|
+
for name in current.list_commands(_CLICK_CONTEXT(current))
|
|
469
|
+
if (child := current.get_command(_CLICK_CONTEXT(current), name)) is not None
|
|
464
470
|
and not child.hidden
|
|
465
471
|
]
|
|
466
472
|
command_name = " ".join(["compshare", *path])
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from compshare_cli.api import call, call_captured, collect_pages
|
|
9
|
+
from compshare_cli.commands.common import confirm_details, request, runtime
|
|
10
|
+
from compshare_cli.errors import UsageError
|
|
11
|
+
from compshare_cli.i18n import tr
|
|
12
|
+
from compshare_cli.location import locate_instances, supported_locations
|
|
13
|
+
from compshare_cli.output import Renderer
|
|
14
|
+
from compshare_cli.parsing import compact
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Manage dedicated bandwidth.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
_BANDWIDTH_COLUMNS = (
|
|
19
|
+
("ShareBandwidthId", "BANDWIDTH ID"),
|
|
20
|
+
("Region", "REGION"),
|
|
21
|
+
("Zone", "ZONE"),
|
|
22
|
+
("Bandwidth", "BANDWIDTH (Mbps)"),
|
|
23
|
+
("Status", "STATUS"),
|
|
24
|
+
("ChargeType", "CHARGE"),
|
|
25
|
+
("BindEIPCount", "EIPS"),
|
|
26
|
+
("PurchaseValue", "EXPIRES"),
|
|
27
|
+
)
|
|
28
|
+
_BANDWIDTH_JSON_FIELDS = (
|
|
29
|
+
"ShareBandwidthId",
|
|
30
|
+
"ResourceId",
|
|
31
|
+
"Region",
|
|
32
|
+
"Zone",
|
|
33
|
+
"Bandwidth",
|
|
34
|
+
"Status",
|
|
35
|
+
"ChargeType",
|
|
36
|
+
"Quantity",
|
|
37
|
+
"PurchaseValue",
|
|
38
|
+
"BindEIPCount",
|
|
39
|
+
"BindEIPInfo",
|
|
40
|
+
"CreateTime",
|
|
41
|
+
"UpdateTime",
|
|
42
|
+
)
|
|
43
|
+
_EIP_COLUMNS = (
|
|
44
|
+
("UHostId", "INSTANCE"),
|
|
45
|
+
("Name", "NAME"),
|
|
46
|
+
("Zone", "ZONE"),
|
|
47
|
+
("IP", "PUBLIC IP"),
|
|
48
|
+
("EIPId", "EIP ID"),
|
|
49
|
+
("Scope", "BANDWIDTH SCOPE"),
|
|
50
|
+
("Bandwidth", "BANDWIDTH (Mbps)"),
|
|
51
|
+
("CanSwitch", "CAN SWITCH"),
|
|
52
|
+
)
|
|
53
|
+
_EIP_JSON_FIELDS = tuple(key for key, _ in _EIP_COLUMNS) + (
|
|
54
|
+
"Region",
|
|
55
|
+
"ShareBandwidthId",
|
|
56
|
+
"TargetScope",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _charge_type(value: str) -> str:
|
|
61
|
+
charge = {"month": "Month", "year": "Year"}.get(value.casefold())
|
|
62
|
+
if charge is None:
|
|
63
|
+
raise UsageError(tr("--charge must be Month or Year."))
|
|
64
|
+
return charge
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _target_scope(value: str) -> Tuple[str, str]:
|
|
68
|
+
target = {
|
|
69
|
+
"company": ("Company", "dedicated"),
|
|
70
|
+
"dedicated": ("Company", "dedicated"),
|
|
71
|
+
"public": ("Public", "shared"),
|
|
72
|
+
"shared": ("Public", "shared"),
|
|
73
|
+
}.get(value.casefold())
|
|
74
|
+
if target is None:
|
|
75
|
+
raise UsageError(tr("--to must be dedicated or shared."))
|
|
76
|
+
return target
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cny(cents: Any, *, signed: bool = False) -> Optional[str]:
|
|
80
|
+
if isinstance(cents, bool) or not isinstance(cents, (int, float)):
|
|
81
|
+
return None
|
|
82
|
+
amount = Decimal(str(cents)) / 100
|
|
83
|
+
if signed and amount < 0:
|
|
84
|
+
return tr("Refund {amount} CNY", amount=f"{abs(amount):.2f}")
|
|
85
|
+
if signed and amount > 0:
|
|
86
|
+
return tr("Payment {amount} CNY", amount=f"{amount:.2f}")
|
|
87
|
+
return f"{amount:.2f} CNY"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _request(
|
|
91
|
+
ctx: typer.Context,
|
|
92
|
+
region: str,
|
|
93
|
+
zone: str,
|
|
94
|
+
values: Optional[Dict[str, Any]] = None,
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
params = request(ctx, zone=True, region_value=region, zone_value=zone)
|
|
97
|
+
params.update(values or {})
|
|
98
|
+
return params
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _bandwidth_rows(response: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
|
|
102
|
+
return response.get("CompanyShareBandwidthSet") or []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _eip_rows(response: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
|
|
106
|
+
for host in response.get("UHostSet") or []:
|
|
107
|
+
for ip in host.get("IPSet") or []:
|
|
108
|
+
if not ip.get("IPId") or ip.get("Type") in {"Private", "Proxy"}:
|
|
109
|
+
continue
|
|
110
|
+
assignment = ip.get("ShareBandwidth") or {}
|
|
111
|
+
yield {
|
|
112
|
+
"UHostId": host.get("UHostId"),
|
|
113
|
+
"Name": host.get("Name"),
|
|
114
|
+
"Region": host.get("Region"),
|
|
115
|
+
"Zone": host.get("Zone"),
|
|
116
|
+
"IP": ip.get("IP"),
|
|
117
|
+
"EIPId": ip.get("IPId"),
|
|
118
|
+
"Scope": assignment.get("Scope", "Unknown"),
|
|
119
|
+
"ShareBandwidthId": assignment.get("ShareBandwidthId"),
|
|
120
|
+
"Bandwidth": assignment.get("Bandwidth"),
|
|
121
|
+
"CanSwitch": bool(assignment.get("CanSwitch")),
|
|
122
|
+
"TargetScope": assignment.get("TargetScope"),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("list", help="List dedicated bandwidth resources.")
|
|
127
|
+
def list_bandwidths(
|
|
128
|
+
ctx: typer.Context,
|
|
129
|
+
region: Optional[str] = typer.Option(None, "--region", help="Filter by region."),
|
|
130
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Filter by availability zone."),
|
|
131
|
+
) -> None:
|
|
132
|
+
if zone is not None and region is None:
|
|
133
|
+
raise UsageError(tr("--zone requires --region."))
|
|
134
|
+
state = runtime(ctx)
|
|
135
|
+
locations = [
|
|
136
|
+
item
|
|
137
|
+
for item in supported_locations(state, request_region=region)
|
|
138
|
+
if not item.get("IsPod") and (zone is None or item.get("Zone") == zone)
|
|
139
|
+
]
|
|
140
|
+
rows: List[Dict[str, Any]] = []
|
|
141
|
+
for location in locations:
|
|
142
|
+
location_region = str(location["Region"])
|
|
143
|
+
location_zone = str(location["Zone"])
|
|
144
|
+
response = call(
|
|
145
|
+
state,
|
|
146
|
+
"DescribeCompShareShareBandwidth",
|
|
147
|
+
_request(ctx, location_region, location_zone),
|
|
148
|
+
)
|
|
149
|
+
bandwidth = response.get("CompanyShareBandwidth")
|
|
150
|
+
if bandwidth:
|
|
151
|
+
rows.append(
|
|
152
|
+
{
|
|
153
|
+
**dict(bandwidth),
|
|
154
|
+
"Region": location_region,
|
|
155
|
+
"Zone": location_zone,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
response = {
|
|
159
|
+
"CompanyShareBandwidthSet": rows,
|
|
160
|
+
"TotalCount": len(rows),
|
|
161
|
+
"ReturnedCount": len(rows),
|
|
162
|
+
}
|
|
163
|
+
Renderer(state.json_output, state.show_sensitive).data(
|
|
164
|
+
response,
|
|
165
|
+
rows=_bandwidth_rows(response),
|
|
166
|
+
columns=_BANDWIDTH_COLUMNS,
|
|
167
|
+
json_list=True,
|
|
168
|
+
json_fields=_BANDWIDTH_JSON_FIELDS,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command("instances", help="List instance EIP bandwidth assignments.")
|
|
173
|
+
def instances(
|
|
174
|
+
ctx: typer.Context,
|
|
175
|
+
ids: Optional[List[str]] = typer.Option(None, "--id", help="Instance ID; repeatable."),
|
|
176
|
+
region: Optional[str] = typer.Option(None, "--region", help="Filter by region."),
|
|
177
|
+
zone: Optional[str] = typer.Option(None, "--zone", help="Filter by availability zone."),
|
|
178
|
+
) -> None:
|
|
179
|
+
if (region is None) != (zone is None):
|
|
180
|
+
raise UsageError(tr("--region and --zone must be provided together."))
|
|
181
|
+
state = runtime(ctx)
|
|
182
|
+
params = request(ctx, region_value=region)
|
|
183
|
+
params.update(
|
|
184
|
+
compact(
|
|
185
|
+
{
|
|
186
|
+
"Zone": zone,
|
|
187
|
+
"UHostIds": ids,
|
|
188
|
+
"SkipBill": True,
|
|
189
|
+
"IncludeShareBandwidth": True,
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
response = collect_pages(state, "DescribeCompShareInstance", params, "UHostSet")
|
|
194
|
+
rows = list(_eip_rows(response))
|
|
195
|
+
result = {
|
|
196
|
+
"EIPSet": rows,
|
|
197
|
+
"TotalCount": len(rows),
|
|
198
|
+
"ReturnedCount": len(rows),
|
|
199
|
+
}
|
|
200
|
+
Renderer(state.json_output, state.show_sensitive).data(
|
|
201
|
+
result,
|
|
202
|
+
rows=rows,
|
|
203
|
+
columns=_EIP_COLUMNS,
|
|
204
|
+
json_list=True,
|
|
205
|
+
json_fields=_EIP_JSON_FIELDS,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command("create", help="Purchase dedicated bandwidth after quoting the price.")
|
|
210
|
+
def create(
|
|
211
|
+
ctx: typer.Context,
|
|
212
|
+
bandwidth: int = typer.Option(
|
|
213
|
+
...,
|
|
214
|
+
"--bandwidth",
|
|
215
|
+
min=20,
|
|
216
|
+
max=10000,
|
|
217
|
+
help="Bandwidth limit in Mbps (20-10000).",
|
|
218
|
+
),
|
|
219
|
+
charge: str = typer.Option("Month", help="Billing type: Month or Year."),
|
|
220
|
+
quantity: int = typer.Option(1, min=1, help="Billing duration for prepaid modes."),
|
|
221
|
+
region: str = typer.Option(..., "--region", help="Region for this request."),
|
|
222
|
+
zone: str = typer.Option(..., "--zone", help="Availability zone."),
|
|
223
|
+
dry_run: bool = typer.Option(
|
|
224
|
+
False,
|
|
225
|
+
"--dry-run",
|
|
226
|
+
help="Quote and show the request without purchasing bandwidth.",
|
|
227
|
+
),
|
|
228
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
229
|
+
) -> None:
|
|
230
|
+
state = runtime(ctx)
|
|
231
|
+
params = _request(
|
|
232
|
+
ctx,
|
|
233
|
+
region,
|
|
234
|
+
zone,
|
|
235
|
+
{
|
|
236
|
+
"ShareBandwidth": bandwidth,
|
|
237
|
+
"ChargeType": _charge_type(charge),
|
|
238
|
+
"Quantity": quantity,
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
quote = call(state, "GetCompShareShareBandwidthPrice", params)
|
|
242
|
+
plan = {"dry_run": dry_run, "request": params, "price": quote}
|
|
243
|
+
fields = [
|
|
244
|
+
("ZONE", zone),
|
|
245
|
+
("BANDWIDTH", f"{bandwidth} Mbps"),
|
|
246
|
+
("CHARGE", params["ChargeType"]),
|
|
247
|
+
("QUANTITY", quantity),
|
|
248
|
+
("PRICE", _cny(quote.get("TotalPrice"))),
|
|
249
|
+
]
|
|
250
|
+
if dry_run:
|
|
251
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
252
|
+
"Create plan", fields, response=plan
|
|
253
|
+
)
|
|
254
|
+
return
|
|
255
|
+
confirm_details(state, "Create plan", fields, "Confirm this operation?", yes)
|
|
256
|
+
created = call(state, "CreateCompShareShareBandwidth", params)
|
|
257
|
+
result = {"price": quote, "created": created}
|
|
258
|
+
resource = created.get("CompanyShareBandwidth") or {}
|
|
259
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
260
|
+
"Operation completed",
|
|
261
|
+
[
|
|
262
|
+
("BANDWIDTH ID", resource.get("ShareBandwidthId")),
|
|
263
|
+
("BANDWIDTH", f"{resource.get('Bandwidth', bandwidth)} Mbps"),
|
|
264
|
+
("ORDER", created.get("OrderNo")),
|
|
265
|
+
],
|
|
266
|
+
response=result,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command("resize", help="Change a dedicated bandwidth limit after quoting the price.")
|
|
271
|
+
def resize(
|
|
272
|
+
ctx: typer.Context,
|
|
273
|
+
bandwidth_id: str = typer.Argument(..., help="Dedicated bandwidth ID."),
|
|
274
|
+
bandwidth: int = typer.Option(
|
|
275
|
+
...,
|
|
276
|
+
"--bandwidth",
|
|
277
|
+
min=20,
|
|
278
|
+
max=10000,
|
|
279
|
+
help="Target bandwidth limit in Mbps (20-10000).",
|
|
280
|
+
),
|
|
281
|
+
region: str = typer.Option(..., "--region", help="Region for this request."),
|
|
282
|
+
zone: str = typer.Option(..., "--zone", help="Availability zone."),
|
|
283
|
+
dry_run: bool = typer.Option(
|
|
284
|
+
False,
|
|
285
|
+
"--dry-run",
|
|
286
|
+
help="Quote and show the request without changing bandwidth.",
|
|
287
|
+
),
|
|
288
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
289
|
+
) -> None:
|
|
290
|
+
state = runtime(ctx)
|
|
291
|
+
params = _request(
|
|
292
|
+
ctx,
|
|
293
|
+
region,
|
|
294
|
+
zone,
|
|
295
|
+
{"ShareBandwidthId": bandwidth_id, "ShareBandwidth": bandwidth},
|
|
296
|
+
)
|
|
297
|
+
quote = call(state, "GetCompShareShareBandwidthUpgradePrice", params)
|
|
298
|
+
plan = {"dry_run": dry_run, "request": params, "price": quote}
|
|
299
|
+
fields = [
|
|
300
|
+
("BANDWIDTH ID", bandwidth_id),
|
|
301
|
+
("ZONE", zone),
|
|
302
|
+
("TARGET BANDWIDTH", f"{bandwidth} Mbps"),
|
|
303
|
+
("PRICE", _cny(quote.get("TotalPrice"), signed=True)),
|
|
304
|
+
]
|
|
305
|
+
if dry_run:
|
|
306
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
307
|
+
"Operation plan", fields, response=plan
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
confirm_details(state, "Operation plan", fields, "Confirm this operation?", yes)
|
|
311
|
+
modified = call(state, "ModifyCompShareShareBandwidth", params)
|
|
312
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
313
|
+
"Operation completed",
|
|
314
|
+
[("BANDWIDTH ID", bandwidth_id), ("BANDWIDTH", f"{bandwidth} Mbps")],
|
|
315
|
+
response={"price": quote, "modified": modified},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@app.command("delete", help="Delete dedicated bandwidth after quoting the refund.")
|
|
320
|
+
def delete(
|
|
321
|
+
ctx: typer.Context,
|
|
322
|
+
bandwidth_id: str = typer.Argument(..., help="Dedicated bandwidth ID."),
|
|
323
|
+
region: str = typer.Option(..., "--region", help="Region for this request."),
|
|
324
|
+
zone: str = typer.Option(..., "--zone", help="Availability zone."),
|
|
325
|
+
dry_run: bool = typer.Option(
|
|
326
|
+
False,
|
|
327
|
+
"--dry-run",
|
|
328
|
+
help="Quote and show the request without deleting bandwidth.",
|
|
329
|
+
),
|
|
330
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
331
|
+
) -> None:
|
|
332
|
+
state = runtime(ctx)
|
|
333
|
+
params = _request(ctx, region, zone, {"ShareBandwidthId": bandwidth_id})
|
|
334
|
+
quote = call(state, "GetCompShareShareBandwidthRefundPrice", params)
|
|
335
|
+
plan = {"dry_run": dry_run, "request": params, "refund": quote}
|
|
336
|
+
fields = [
|
|
337
|
+
("BANDWIDTH ID", bandwidth_id),
|
|
338
|
+
("ZONE", zone),
|
|
339
|
+
("REFUND", _cny(quote.get("RefundPrice"), signed=True)),
|
|
340
|
+
("ACTION", tr("Migrate bound EIPs to shared bandwidth, then delete")),
|
|
341
|
+
]
|
|
342
|
+
if dry_run:
|
|
343
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
344
|
+
"Operation plan", fields, response=plan
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
confirm_details(state, "Operation plan", fields, "Confirm this operation?", yes)
|
|
348
|
+
deleted = call(state, "DeleteCompShareShareBandwidth", params)
|
|
349
|
+
Renderer(state.json_output, state.show_sensitive).details(
|
|
350
|
+
"Operation completed",
|
|
351
|
+
[
|
|
352
|
+
("BANDWIDTH ID", bandwidth_id),
|
|
353
|
+
("MIGRATED EIPS", deleted.get("MigratedEIPCount", 0)),
|
|
354
|
+
],
|
|
355
|
+
response={"refund": quote, "deleted": deleted},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@app.command("switch", help="Switch instance EIPs between dedicated and shared bandwidth.")
|
|
360
|
+
def switch(
|
|
361
|
+
ctx: typer.Context,
|
|
362
|
+
instances: List[str] = typer.Argument(..., help="Instance IDs."),
|
|
363
|
+
target: str = typer.Option(..., "--to", help="Target bandwidth: dedicated or shared."),
|
|
364
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
365
|
+
) -> None:
|
|
366
|
+
state = runtime(ctx)
|
|
367
|
+
target_scope, target_name = _target_scope(target)
|
|
368
|
+
requested = list(dict.fromkeys(instances))
|
|
369
|
+
locations, missing = locate_instances(state, requested)
|
|
370
|
+
confirm_details(
|
|
371
|
+
state,
|
|
372
|
+
"Operation plan",
|
|
373
|
+
[("INSTANCE", requested), ("TARGET", tr(target_name))],
|
|
374
|
+
"Confirm this operation?",
|
|
375
|
+
yes,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
results: List[Dict[str, Any]] = [
|
|
379
|
+
{
|
|
380
|
+
"UHostId": instance,
|
|
381
|
+
"Success": False,
|
|
382
|
+
"Message": tr("Instance {instance} was not found.", instance=instance),
|
|
383
|
+
}
|
|
384
|
+
for instance in missing
|
|
385
|
+
]
|
|
386
|
+
groups: Dict[Tuple[str, str], List[str]] = {}
|
|
387
|
+
for instance in requested:
|
|
388
|
+
location = locations.get(instance)
|
|
389
|
+
if location is not None:
|
|
390
|
+
groups.setdefault((location[0], location[1]), []).append(instance)
|
|
391
|
+
|
|
392
|
+
for (region, zone), ids in groups.items():
|
|
393
|
+
response, error = call_captured(
|
|
394
|
+
state,
|
|
395
|
+
"SwitchCompShareEIPShareBandwidth",
|
|
396
|
+
_request(
|
|
397
|
+
ctx,
|
|
398
|
+
region,
|
|
399
|
+
zone,
|
|
400
|
+
{"UHostIds": ids, "TargetScope": target_scope},
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
if error:
|
|
404
|
+
results.extend(
|
|
405
|
+
{
|
|
406
|
+
"UHostId": instance,
|
|
407
|
+
"Region": region,
|
|
408
|
+
"Zone": zone,
|
|
409
|
+
"Success": False,
|
|
410
|
+
"Message": error.get("message"),
|
|
411
|
+
}
|
|
412
|
+
for instance in ids
|
|
413
|
+
)
|
|
414
|
+
continue
|
|
415
|
+
returned = {
|
|
416
|
+
str(item.get("UHostId")): item for item in (response or {}).get("Results") or []
|
|
417
|
+
}
|
|
418
|
+
for instance in ids:
|
|
419
|
+
item = returned.get(instance) or {
|
|
420
|
+
"UHostId": instance,
|
|
421
|
+
"Success": False,
|
|
422
|
+
"Message": tr(
|
|
423
|
+
"The API returned no result for instance {instance}.", instance=instance
|
|
424
|
+
),
|
|
425
|
+
}
|
|
426
|
+
results.append({**item, "Region": region, "Zone": zone})
|
|
427
|
+
|
|
428
|
+
failed = [item for item in results if not item.get("Success")]
|
|
429
|
+
payload: Dict[str, Any] = {"Results": results}
|
|
430
|
+
if failed:
|
|
431
|
+
payload.update(
|
|
432
|
+
{
|
|
433
|
+
"ok": False,
|
|
434
|
+
"error": {
|
|
435
|
+
"code": "partial_failure",
|
|
436
|
+
"message": tr("Some bandwidth switches failed."),
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
Renderer(state.json_output, state.show_sensitive).data(
|
|
441
|
+
payload,
|
|
442
|
+
rows=results,
|
|
443
|
+
columns=(
|
|
444
|
+
("UHostId", "INSTANCE"),
|
|
445
|
+
("Zone", "ZONE"),
|
|
446
|
+
("Success", "SUCCESS"),
|
|
447
|
+
("CurrentScope", "BANDWIDTH SCOPE"),
|
|
448
|
+
("Message", "MESSAGE"),
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
if failed:
|
|
452
|
+
raise typer.Exit(1)
|