ctyun-cli 1.26.0__tar.gz → 1.26.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {ctyun_cli-1.26.0/src/ctyun_cli.egg-info → ctyun_cli-1.26.2}/PKG-INFO +8 -2
  2. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/README.md +7 -1
  3. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/pyproject.toml +1 -1
  4. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/setup.py +1 -1
  5. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2/src/ctyun_cli.egg-info}/PKG-INFO +8 -2
  6. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ebs/client.py +96 -3
  7. ctyun_cli-1.26.2/src/ebs/commands.py +308 -0
  8. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/vpc/client.py +197 -6
  9. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/vpc/commands.py +239 -0
  10. ctyun_cli-1.26.0/src/ebs/commands.py +0 -165
  11. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/LICENSE +0 -0
  12. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/MANIFEST.in +0 -0
  13. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/PROJECT_SUMMARY.md +0 -0
  14. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/requirements.txt +0 -0
  15. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/setup.cfg +0 -0
  16. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/__init__.py +0 -0
  17. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aiserver/__init__.py +0 -0
  18. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aiserver/client.py +0 -0
  19. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aiserver/commands.py +0 -0
  20. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aone/__init__.py +0 -0
  21. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aone/client.py +0 -0
  22. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/aone/commands.py +0 -0
  23. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/apm/__init__.py +0 -0
  24. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/apm/client.py +0 -0
  25. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/apm/commands.py +0 -0
  26. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/audit/__init__.py +0 -0
  27. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/audit/client.py +0 -0
  28. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/audit/commands.py +0 -0
  29. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/auth/__init__.py +0 -0
  30. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/auth/eop_signature.py +0 -0
  31. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/auth/signature.py +0 -0
  32. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/billing/__init__.py +0 -0
  33. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/billing/client.py +0 -0
  34. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/billing/commands.py +0 -0
  35. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cce/__init__.py +0 -0
  36. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cce/client.py +0 -0
  37. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cce/commands.py +0 -0
  38. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cda/__init__.py +0 -0
  39. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cda/client.py +0 -0
  40. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cda/commands.py +0 -0
  41. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cli/__init__.py +0 -0
  42. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cli/main.py +0 -0
  43. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cloudpc/__init__.py +0 -0
  44. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cloudpc/client.py +0 -0
  45. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/cloudpc/commands.py +0 -0
  46. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/config/__init__.py +0 -0
  47. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/config/settings.py +0 -0
  48. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/core/__init__.py +0 -0
  49. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/css/__init__.py +0 -0
  50. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/css/client.py +0 -0
  51. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/css/commands.py +0 -0
  52. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/csscn/__init__.py +0 -0
  53. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/csscn/client.py +0 -0
  54. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/csscn/commands.py +0 -0
  55. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctmysql/__init__.py +0 -0
  56. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctmysql/client.py +0 -0
  57. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctmysql/commands.py +0 -0
  58. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli/__init__.py +0 -0
  59. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/SOURCES.txt +0 -0
  60. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/dependency_links.txt +0 -0
  61. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/entry_points.txt +0 -0
  62. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/not-zip-safe +0 -0
  63. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/requires.txt +0 -0
  64. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ctyun_cli.egg-info/top_level.txt +0 -0
  65. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/dps/__init__.py +0 -0
  66. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/dps/client.py +0 -0
  67. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/dps/commands.py +0 -0
  68. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ebs/__init__.py +0 -0
  69. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ecs/__init__.py +0 -0
  70. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ecs/client.py +0 -0
  71. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ecs/commands.py +0 -0
  72. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/elb/__init__.py +0 -0
  73. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/elb/client.py +0 -0
  74. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/elb/commands.py +0 -0
  75. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/emr/__init__.py +0 -0
  76. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/emr/client.py +0 -0
  77. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/emr/commands.py +0 -0
  78. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/iam/__init__.py +0 -0
  79. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/iam/client.py +0 -0
  80. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/iam/commands.py +0 -0
  81. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ims/__init__.py +0 -0
  82. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ims/client.py +0 -0
  83. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/ims/commands.py +0 -0
  84. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/kafka/__init__.py +0 -0
  85. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/kafka/client.py +0 -0
  86. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/kafka/commands.py +0 -0
  87. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/lts/__init__.py +0 -0
  88. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/lts/client.py +0 -0
  89. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/lts/commands.py +0 -0
  90. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/monitor/__init__.py +0 -0
  91. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/monitor/client.py +0 -0
  92. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/monitor/commands.py +0 -0
  93. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/oceanfs/__init__.py +0 -0
  94. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/oceanfs/client.py +0 -0
  95. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/oceanfs/commands.py +0 -0
  96. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/rdscmd/__init__.py +0 -0
  97. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/rdscmd/client.py +0 -0
  98. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/rdscmd/commands.py +0 -0
  99. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/security/__init__.py +0 -0
  100. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/security/client.py +0 -0
  101. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/security/commands.py +0 -0
  102. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/sfs/__init__.py +0 -0
  103. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/sfs/client.py +0 -0
  104. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/sfs/commands.py +0 -0
  105. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/utils/__init__.py +0 -0
  106. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/utils/cache.py +0 -0
  107. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/utils/helpers.py +0 -0
  108. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/vpc/__init__.py +0 -0
  109. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/zos/__init__.py +0 -0
  110. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/zos/client.py +0 -0
  111. {ctyun_cli-1.26.0 → ctyun_cli-1.26.2}/src/zos/commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctyun-cli
3
- Version: 1.26.0
3
+ Version: 1.26.2
4
4
  Summary: 天翼云CLI工具 - 基于终端的云资源管理平台
5
5
  Home-page: https://github.com/fengyucn/ctyun-cli
6
6
  Author: Y.FENG
@@ -217,7 +217,13 @@ ctyun-cli cce list-clusters
217
217
 
218
218
  ## 📝 更新日志
219
219
 
220
- **最新版本**: v1.26.0 (2026-07-20)
220
+ **最新版本**: v1.26.2 (2026-07-21)
221
+ - 🚀 **VPC 模块新增 13 个命令**:NAT 网关、SNAT/DNAT、对等连接、路由表、安全组规则、端口管理
222
+
223
+ **v1.26.1** (2026-07-21)
224
+ - 🚀 **EBS 模块新增 6 个命令**:云硬盘详情/按名称查询、快照列表/容量/策略
225
+
226
+ **v1.26.0** (2026-07-20)
221
227
  - 🚀 **Redis 模块大幅扩展**:新增 52 个命令,涵盖备份管理、安全配置、Key 分析、监控诊断、参数模板、迁移任务等
222
228
 
223
229
  **v1.25.0** (2026-07-17)
@@ -154,7 +154,13 @@ ctyun-cli cce list-clusters
154
154
 
155
155
  ## 📝 更新日志
156
156
 
157
- **最新版本**: v1.26.0 (2026-07-20)
157
+ **最新版本**: v1.26.2 (2026-07-21)
158
+ - 🚀 **VPC 模块新增 13 个命令**:NAT 网关、SNAT/DNAT、对等连接、路由表、安全组规则、端口管理
159
+
160
+ **v1.26.1** (2026-07-21)
161
+ - 🚀 **EBS 模块新增 6 个命令**:云硬盘详情/按名称查询、快照列表/容量/策略
162
+
163
+ **v1.26.0** (2026-07-20)
158
164
  - 🚀 **Redis 模块大幅扩展**:新增 52 个命令,涵盖备份管理、安全配置、Key 分析、监控诊断、参数模板、迁移任务等
159
165
 
160
166
  **v1.25.0** (2026-07-17)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ctyun-cli"
7
- version = "1.26.0"
7
+ version = "1.26.2"
8
8
  description = "天翼云CLI工具 - 基于终端的云资源管理平台"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -45,7 +45,7 @@ DEV_REQUIREMENTS = [
45
45
  setup(
46
46
  # 基本包信息
47
47
  name="ctyun-cli",
48
- version="1.26.0",
48
+ version="1.26.2",
49
49
  description="天翼云CLI工具 - 基于终端的云资源管理平台",
50
50
  long_description=read_file('README.md'),
51
51
  long_description_content_type="text/markdown",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctyun-cli
3
- Version: 1.26.0
3
+ Version: 1.26.2
4
4
  Summary: 天翼云CLI工具 - 基于终端的云资源管理平台
5
5
  Home-page: https://github.com/fengyucn/ctyun-cli
6
6
  Author: Y.FENG
@@ -217,7 +217,13 @@ ctyun-cli cce list-clusters
217
217
 
218
218
  ## 📝 更新日志
219
219
 
220
- **最新版本**: v1.26.0 (2026-07-20)
220
+ **最新版本**: v1.26.2 (2026-07-21)
221
+ - 🚀 **VPC 模块新增 13 个命令**:NAT 网关、SNAT/DNAT、对等连接、路由表、安全组规则、端口管理
222
+
223
+ **v1.26.1** (2026-07-21)
224
+ - 🚀 **EBS 模块新增 6 个命令**:云硬盘详情/按名称查询、快照列表/容量/策略
225
+
226
+ **v1.26.0** (2026-07-20)
221
227
  - 🚀 **Redis 模块大幅扩展**:新增 52 个命令,涵盖备份管理、安全配置、Key 分析、监控诊断、参数模板、迁移任务等
222
228
 
223
229
  **v1.25.0** (2026-07-17)
@@ -127,12 +127,12 @@ class EBSClient:
127
127
  }
128
128
 
129
129
  result = response.json()
130
-
130
+
131
131
  if result.get('statusCode') != 800:
132
132
  logger.warning(f"API返回错误: {result.get('message', '未知错误')}")
133
-
133
+
134
134
  return result
135
-
135
+
136
136
  except Exception as e:
137
137
  logger.error(f"查询云硬盘列表失败: {e}")
138
138
  import traceback
@@ -142,3 +142,96 @@ class EBSClient:
142
142
  'message': str(e),
143
143
  'returnObj': None
144
144
  }
145
+
146
+ # ==================== 新增查询类 API(6个) ====================
147
+
148
+ def _get(self, path: str, query_params: Dict) -> Dict[str, Any]:
149
+ """通用 GET 请求"""
150
+ url = f'https://{self.base_endpoint}{path}'
151
+ filtered = {k: v for k, v in query_params.items() if v is not None}
152
+ try:
153
+ headers = self.eop_auth.sign_request(
154
+ method='GET', url=url, query_params=filtered, body='',
155
+ extra_headers={}
156
+ )
157
+ logger.debug(f"请求URL: {url}")
158
+ logger.debug(f"查询参数: {filtered}")
159
+
160
+ response = self.client.session.get(
161
+ url, params=filtered, headers=headers, timeout=30
162
+ )
163
+
164
+ logger.debug(f"响应状态码: {response.status_code}")
165
+
166
+ if response.status_code != 200:
167
+ return {
168
+ 'statusCode': response.status_code,
169
+ 'message': f'HTTP {response.status_code}: {response.text}',
170
+ 'returnObj': None
171
+ }
172
+ return response.json()
173
+ except Exception as e:
174
+ logger.error(f"GET {path} 失败: {e}")
175
+ return {'statusCode': 500, 'message': str(e), 'returnObj': None}
176
+
177
+ def get_ebs_info(self, region_id: str, disk_id: str) -> Dict[str, Any]:
178
+ """查询云硬盘详情(基于diskID)- GET /v4/ebs/info-ebs"""
179
+ logger.info(f"查询云硬盘详情: regionId={region_id}, diskID={disk_id}")
180
+ return self._get('/v4/ebs/info-ebs', {
181
+ 'regionID': region_id, 'diskID': disk_id,
182
+ })
183
+
184
+ def get_ebs_info_by_name(self, region_id: str, disk_name: str) -> Dict[str, Any]:
185
+ """查询云硬盘详情(基于regionID和diskName)- GET /v4/ebs/info-by-name-ebs"""
186
+ logger.info(f"查询云硬盘详情(by name): regionId={region_id}, diskName={disk_name}")
187
+ return self._get('/v4/ebs/info-by-name-ebs', {
188
+ 'regionID': region_id, 'diskName': disk_name,
189
+ })
190
+
191
+ def list_ebs_by_name(self, region_id: str, disk_name: str,
192
+ page_no: Optional[int] = None,
193
+ page_size: Optional[int] = None) -> Dict[str, Any]:
194
+ """查询云硬盘列表(基于regionID和diskName)- GET /v4/ebs/list-by-name-ebs"""
195
+ logger.info(f"查询云硬盘列表(by name): regionId={region_id}, diskName={disk_name}")
196
+ return self._get('/v4/ebs/list-by-name-ebs', {
197
+ 'regionID': region_id, 'diskName': disk_name,
198
+ 'pageNo': page_no, 'pageSize': page_size,
199
+ })
200
+
201
+ def list_ebs_snapshots(self, region_id: str,
202
+ disk_id: Optional[str] = None,
203
+ snapshot_id: Optional[str] = None,
204
+ snapshot_name: Optional[str] = None,
205
+ snapshot_status: Optional[str] = None,
206
+ snapshot_type: Optional[str] = None,
207
+ volume_attr: Optional[str] = None,
208
+ retention_policy: Optional[str] = None,
209
+ max_results: Optional[int] = None,
210
+ next_token: Optional[str] = None) -> Dict[str, Any]:
211
+ """查询云硬盘快照列表 - GET /v4/ebs_snapshot/list-ebs-snap"""
212
+ logger.info(f"查询云硬盘快照列表: regionId={region_id}")
213
+ return self._get('/v4/ebs_snapshot/list-ebs-snap', {
214
+ 'regionID': region_id, 'diskID': disk_id, 'snapshotID': snapshot_id,
215
+ 'snapshotName': snapshot_name, 'snapshotStatus': snapshot_status,
216
+ 'snapshotType': snapshot_type, 'volumeAttr': volume_attr,
217
+ 'retentionPolicy': retention_policy,
218
+ 'maxResults': max_results, 'nextToken': next_token,
219
+ })
220
+
221
+ def query_ebs_snapshot_size(self, region_id: str) -> Dict[str, Any]:
222
+ """查询云硬盘快照使用量 - GET /v4/ebs_snapshot/query_size-ebs-snap"""
223
+ logger.info(f"查询云硬盘快照使用量: regionId={region_id}")
224
+ return self._get('/v4/ebs_snapshot/query_size-ebs-snap', {
225
+ 'regionID': region_id,
226
+ })
227
+
228
+ def query_ebs_snapshot_policy(self, region_id: str,
229
+ snapshot_policy_id: Optional[str] = None,
230
+ snapshot_policy_name: Optional[str] = None) -> Dict[str, Any]:
231
+ """查询云硬盘自动快照策略 - GET /v4/ebs_snapshot/query-policy-ebs-snap"""
232
+ logger.info(f"查询云硬盘自动快照策略: regionId={region_id}")
233
+ return self._get('/v4/ebs_snapshot/query-policy-ebs-snap', {
234
+ 'regionID': region_id,
235
+ 'snapshotPolicyID': snapshot_policy_id,
236
+ 'snapshotPolicyName': snapshot_policy_name,
237
+ })
@@ -0,0 +1,308 @@
1
+ """
2
+ 云硬盘(EBS)命令行接口
3
+ """
4
+
5
+ import click
6
+ from functools import wraps
7
+ from typing import Optional
8
+ from .client import EBSClient
9
+ from utils import OutputFormatter
10
+
11
+
12
+ def handle_error(func):
13
+ """错误处理装饰器"""
14
+ @wraps(func)
15
+ def wrapper(*args, **kwargs):
16
+ try:
17
+ return func(*args, **kwargs)
18
+ except Exception as e:
19
+ import sys
20
+ click.echo(f"错误: {e}", err=True)
21
+ sys.exit(1)
22
+ return wrapper
23
+
24
+
25
+ def format_output(data, output_format='table'):
26
+ """格式化输出"""
27
+ if output_format == 'json':
28
+ click.echo(OutputFormatter.format_json(data))
29
+ elif output_format == 'yaml':
30
+ try:
31
+ import yaml
32
+ click.echo(yaml.dump(data, allow_unicode=True, default_flow_style=False))
33
+ except ImportError:
34
+ click.echo("错误: 需要安装PyYAML库", err=True)
35
+ import sys
36
+ sys.exit(1)
37
+ else:
38
+ if isinstance(data, list) and data:
39
+ headers = list(data[0].keys())
40
+ table = OutputFormatter.format_table(data, headers)
41
+ click.echo(table)
42
+ elif isinstance(data, dict):
43
+ headers = ['字段', '值']
44
+ table_data = [[key, value] for key, value in data.items()]
45
+ table = OutputFormatter.format_table(table_data, headers)
46
+ click.echo(table)
47
+ else:
48
+ click.echo(data)
49
+
50
+
51
+ @click.group()
52
+ def ebs():
53
+ """云硬盘(EBS)管理"""
54
+ pass
55
+
56
+
57
+ @ebs.command('list')
58
+ @click.option('--region-id', required=True, help='资源池ID')
59
+ @click.option('--page', default=1, type=int, help='页码,默认1')
60
+ @click.option('--page-size', default=10, type=int, help='每页数量,默认10,最大300')
61
+ @click.option('--az-name', help='可用区')
62
+ @click.option('--project-id', help='企业项目ID')
63
+ @click.option('--disk-type', help='云硬盘类型(SATA/SAS/SSD/FAST-SSD)')
64
+ @click.option('--disk-mode', help='云硬盘模式(VBD/ISCSI/FCSAN)')
65
+ @click.option('--disk-status', help='云硬盘状态(in-use/available等)')
66
+ @click.option('--multi-attach', help='是否共享盘(true/false)')
67
+ @click.option('--is-system-volume', help='是否系统盘(true/false)')
68
+ @click.option('--is-encrypt', help='是否加密盘(true/false)')
69
+ @click.option('--query-content', help='模糊查询内容')
70
+ @click.option('--query-keys', help='模糊查询键(name,diskID,instanceID,instanceName)')
71
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), help='输出格式')
72
+ @click.pass_context
73
+ @handle_error
74
+ def list_ebs(ctx, region_id: str, page: int, page_size: int,
75
+ az_name: Optional[str], project_id: Optional[str],
76
+ disk_type: Optional[str], disk_mode: Optional[str],
77
+ disk_status: Optional[str], multi_attach: Optional[str],
78
+ is_system_volume: Optional[str], is_encrypt: Optional[str],
79
+ query_content: Optional[str], query_keys: Optional[str],
80
+ output: Optional[str]):
81
+ """查询云硬盘列表"""
82
+ try:
83
+ client = ctx.obj['client']
84
+ output_format = output or ctx.obj.get('output', 'table')
85
+
86
+ ebs_client = EBSClient(client)
87
+ result = ebs_client.list_ebs(
88
+ region_id=region_id,
89
+ page_no=page,
90
+ page_size=page_size,
91
+ az_name=az_name,
92
+ project_id=project_id,
93
+ disk_type=disk_type,
94
+ disk_mode=disk_mode,
95
+ disk_status=disk_status,
96
+ multi_attach=multi_attach,
97
+ is_system_volume=is_system_volume,
98
+ is_encrypt=is_encrypt,
99
+ query_content=query_content,
100
+ query_keys=query_keys
101
+ )
102
+
103
+ if result.get('statusCode') != 800:
104
+ error_msg = result.get('message', '未知错误')
105
+ click.echo(f"❌ 查询失败: {error_msg}", err=True)
106
+ import sys
107
+ sys.exit(1)
108
+
109
+ return_obj = result.get('returnObj', {})
110
+ disk_list = return_obj.get('diskList', [])
111
+
112
+ if output_format in ['json', 'yaml']:
113
+ format_output(disk_list, output_format)
114
+ else:
115
+ if disk_list:
116
+ from tabulate import tabulate
117
+ from datetime import datetime
118
+
119
+ table_data = []
120
+ headers = ['云硬盘ID', '名称', '大小(GB)', '类型', '状态', '挂载主机', '创建时间']
121
+
122
+ for disk in disk_list:
123
+ disk_id = disk.get('diskID', '')
124
+ disk_name = disk.get('diskName', '')
125
+ disk_size = disk.get('diskSize', 0)
126
+ disk_type = disk.get('diskType', '')
127
+ disk_status = disk.get('diskStatus', '')
128
+ instance_name = disk.get('instanceName', '-')
129
+
130
+ create_time = disk.get('createTime', 0)
131
+ if isinstance(create_time, int) and create_time > 0:
132
+ try:
133
+ create_time = datetime.fromtimestamp(create_time / 1000).strftime('%Y-%m-%d %H:%M')
134
+ except:
135
+ create_time = str(create_time)
136
+ else:
137
+ create_time = '-'
138
+
139
+ table_data.append([
140
+ disk_id,
141
+ disk_name,
142
+ disk_size,
143
+ disk_type,
144
+ disk_status,
145
+ instance_name,
146
+ create_time
147
+ ])
148
+
149
+ total_count = return_obj.get('totalCount', 0)
150
+ current_count = return_obj.get('currentCount', len(disk_list))
151
+ total_page = return_obj.get('totalPage', 1)
152
+
153
+ click.echo(f"\n云硬盘列表 (总计: {total_count} 个, 当前页: {current_count} 个, 第{page}/{total_page}页)\n")
154
+ table = tabulate(table_data, headers=headers, tablefmt='grid')
155
+ click.echo(table)
156
+
157
+ if page < total_page:
158
+ click.echo(f"\n提示: 使用 --page 参数查看其他页")
159
+ else:
160
+ click.echo("未找到云硬盘")
161
+
162
+ except Exception as e:
163
+ click.echo(f"运行出错: {e}", err=True)
164
+ import traceback
165
+ traceback.print_exc()
166
+
167
+
168
+ # ==================== 新增查询类命令(6个) ====================
169
+
170
+ @ebs.command('info')
171
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
172
+ @click.option('--disk-id', '-d', required=True, help='云硬盘ID')
173
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
174
+ @click.pass_context
175
+ def ebs_info(ctx, region_id, disk_id, output):
176
+ """查询云硬盘详情(基于diskID)"""
177
+ client = EBSClient(ctx.obj['client'])
178
+ result = client.get_ebs_info(region_id=region_id, disk_id=disk_id)
179
+ if output == 'json' or result.get('statusCode') != 800:
180
+ click.echo(OutputFormatter.format_json(result)); return
181
+ ro = result.get('returnObj', {}) or {}
182
+ click.echo(f"云硬盘详情 ({disk_id}):")
183
+ if isinstance(ro, dict):
184
+ for k, v in ro.items():
185
+ click.echo(f" {k}: {v}")
186
+
187
+
188
+ @ebs.command('info-by-name')
189
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
190
+ @click.option('--disk-name', '-n', required=True, help='云硬盘名称')
191
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
192
+ @click.pass_context
193
+ def ebs_info_by_name(ctx, region_id, disk_name, output):
194
+ """查询云硬盘详情(基于regionID和diskName)"""
195
+ client = EBSClient(ctx.obj['client'])
196
+ result = client.get_ebs_info_by_name(region_id=region_id, disk_name=disk_name)
197
+ if output == 'json' or result.get('statusCode') != 800:
198
+ click.echo(OutputFormatter.format_json(result)); return
199
+ ro = result.get('returnObj', {}) or {}
200
+ click.echo(f"云硬盘详情 (name={disk_name}):")
201
+ if isinstance(ro, dict):
202
+ for k, v in ro.items():
203
+ click.echo(f" {k}: {v}")
204
+
205
+
206
+ @ebs.command('list-by-name')
207
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
208
+ @click.option('--disk-name', '-n', required=True, help='云硬盘名称')
209
+ @click.option('--page', type=int, default=1, help='页码,默认1')
210
+ @click.option('--page-size', type=int, default=10, help='每页大小,默认10')
211
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
212
+ @click.pass_context
213
+ def ebs_list_by_name(ctx, region_id, disk_name, page, page_size, output):
214
+ """查询云硬盘列表(基于regionID和diskName)"""
215
+ client = EBSClient(ctx.obj['client'])
216
+ result = client.list_ebs_by_name(
217
+ region_id=region_id, disk_name=disk_name,
218
+ page_no=page, page_size=page_size)
219
+ if output == 'json' or result.get('statusCode') != 800:
220
+ click.echo(OutputFormatter.format_json(result)); return
221
+ ro = result.get('returnObj', {}) or {}
222
+ disk_list = ro.get('ebsList', []) or ro.get('diskList', []) or ro.get('list', []) or (ro if isinstance(ro, list) else [])
223
+ click.echo(f"云硬盘列表 (name={disk_name}, 共 {len(disk_list) if isinstance(disk_list, list) else 0} 个)")
224
+ if isinstance(disk_list, list):
225
+ for d in disk_list:
226
+ if isinstance(d, dict):
227
+ click.echo(f" {d.get('diskID','')} | {d.get('name', d.get('diskName',''))} | {d.get('size','')}GB | {d.get('status','')}")
228
+
229
+
230
+ @ebs.command('snapshots')
231
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
232
+ @click.option('--disk-id', '-d', help='云硬盘ID过滤')
233
+ @click.option('--snapshot-id', help='快照ID过滤')
234
+ @click.option('--snapshot-name', help='快照名称(模糊匹配)')
235
+ @click.option('--snapshot-status',
236
+ type=click.Choice(['available', 'freezing', 'creating', 'deleting', 'rollbacking', 'cloning', 'error']),
237
+ help='快照状态过滤')
238
+ @click.option('--snapshot-type', type=click.Choice(['manu', 'timer']), help='创建类型:manu 手动 / timer 自动')
239
+ @click.option('--volume-attr', type=click.Choice(['data', 'system']), help='云硬盘属性:data 数据盘 / system 系统盘')
240
+ @click.option('--retention-policy', type=click.Choice(['forever', 'custom']), help='保留策略')
241
+ @click.option('--max-results', type=int, help='期望返回数量(默认10,最大1000)')
242
+ @click.option('--next-token', help='接续查询token')
243
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
244
+ @click.pass_context
245
+ def ebs_snapshots(ctx, region_id, disk_id, snapshot_id, snapshot_name,
246
+ snapshot_status, snapshot_type, volume_attr,
247
+ retention_policy, max_results, next_token, output):
248
+ """查询云硬盘快照列表"""
249
+ client = EBSClient(ctx.obj['client'])
250
+ result = client.list_ebs_snapshots(
251
+ region_id=region_id, disk_id=disk_id, snapshot_id=snapshot_id,
252
+ snapshot_name=snapshot_name, snapshot_status=snapshot_status,
253
+ snapshot_type=snapshot_type, volume_attr=volume_attr,
254
+ retention_policy=retention_policy, max_results=max_results,
255
+ next_token=next_token)
256
+ if output == 'json' or result.get('statusCode') != 800:
257
+ click.echo(OutputFormatter.format_json(result)); return
258
+ ro = result.get('returnObj', {}) or {}
259
+ snap_list = ro.get('snapshotList', []) or ro.get('snapshots', []) or ro.get('list', []) or (ro if isinstance(ro, list) else [])
260
+ click.echo(f"云硬盘快照列表 (共 {len(snap_list) if isinstance(snap_list, list) else 0} 个)")
261
+ if isinstance(snap_list, list):
262
+ for s in snap_list:
263
+ if isinstance(s, dict):
264
+ click.echo(f" {s.get('snapshotID','')} | {s.get('snapshotName','')} | {s.get('status','')} | {s.get('size','')}GB")
265
+
266
+
267
+ @ebs.command('snapshot-size')
268
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
269
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
270
+ @click.pass_context
271
+ def ebs_snapshot_size(ctx, region_id, output):
272
+ """查询云硬盘快照使用量"""
273
+ client = EBSClient(ctx.obj['client'])
274
+ result = client.query_ebs_snapshot_size(region_id=region_id)
275
+ if output == 'json' or result.get('statusCode') != 800:
276
+ click.echo(OutputFormatter.format_json(result)); return
277
+ ro = result.get('returnObj', {}) or {}
278
+ click.echo(f"云硬盘快照使用量 ({region_id}):")
279
+ if isinstance(ro, dict):
280
+ for k, v in ro.items():
281
+ click.echo(f" {k}: {v}")
282
+
283
+
284
+ @ebs.command('snapshot-policy')
285
+ @click.option('--region-id', '-r', required=True, help='资源池ID')
286
+ @click.option('--snapshot-policy-id', help='快照策略ID')
287
+ @click.option('--snapshot-policy-name', help='快照策略名称')
288
+ @click.option('--output', type=click.Choice(['table', 'json', 'yaml']), default='table')
289
+ @click.pass_context
290
+ def ebs_snapshot_policy(ctx, region_id, snapshot_policy_id, snapshot_policy_name, output):
291
+ """查询云硬盘自动快照策略"""
292
+ client = EBSClient(ctx.obj['client'])
293
+ result = client.query_ebs_snapshot_policy(
294
+ region_id=region_id, snapshot_policy_id=snapshot_policy_id,
295
+ snapshot_policy_name=snapshot_policy_name)
296
+ if output == 'json' or result.get('statusCode') != 800:
297
+ click.echo(OutputFormatter.format_json(result)); return
298
+ ro = result.get('returnObj', {}) or {}
299
+ policy_list = ro.get('policyList', []) or ro.get('snapshotPolicies', []) or ro.get('list', []) or (ro if isinstance(ro, list) else [])
300
+ if isinstance(policy_list, list) and policy_list:
301
+ click.echo(f"云硬盘自动快照策略列表 (共 {len(policy_list)} 个)")
302
+ for p in policy_list:
303
+ if isinstance(p, dict):
304
+ click.echo(f" {p.get('snapshotPolicyID','')} | {p.get('snapshotPolicyName','')} | status={p.get('status','')}")
305
+ elif isinstance(ro, dict):
306
+ click.echo(f"云硬盘自动快照策略:")
307
+ for k, v in ro.items():
308
+ click.echo(f" {k}: {v}")