dts-dance 0.2.6__tar.gz → 0.2.8__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.
- {dts_dance-0.2.6 → dts_dance-0.2.8}/PKG-INFO +1 -1
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/dflow.py +6 -5
- dts_dance-0.2.8/dtsdance/tce.py +289 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/pyproject.toml +1 -1
- {dts_dance-0.2.6 → dts_dance-0.2.8}/tests/test_dflow.py +3 -1
- {dts_dance-0.2.6 → dts_dance-0.2.8}/uv.lock +1 -1
- {dts_dance-0.2.6 → dts_dance-0.2.8}/.gitignore +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/.python-version +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/CLAUDE.md +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/DEV.md +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/README.md +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/config.yaml +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/__init__.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/bytecloud.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/ddutil.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/dsyncer.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/feishu_base.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/feishu_table.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/metrics_fe.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/s3.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/spacex.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/tcc_inner.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/dtsdance/tcc_open.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/tests/config.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/tests/test_spacex.py +0 -0
- {dts_dance-0.2.6 → dts_dance-0.2.8}/tests/test_tcc.py +0 -0
|
@@ -117,12 +117,13 @@ class DFlowClient:
|
|
|
117
117
|
site_info = self.bytecloud_client.get_site_config(site)
|
|
118
118
|
return f"{site_info.endpoint}/bytedts/datasync/detail/{task_id}"
|
|
119
119
|
|
|
120
|
-
def init_resources(self, site: str, ctrl_env: str) -> bool:
|
|
120
|
+
def init_resources(self, site: str, vregion: str, ctrl_env: str) -> bool:
|
|
121
121
|
"""
|
|
122
122
|
初始化 CTRL 环境资源
|
|
123
123
|
|
|
124
124
|
Args:
|
|
125
125
|
site: 站点名称
|
|
126
|
+
vregion: 虚拟区域
|
|
126
127
|
ctrl_env: 控制环境
|
|
127
128
|
|
|
128
129
|
Returns:
|
|
@@ -135,21 +136,21 @@ class DFlowClient:
|
|
|
135
136
|
# 构建请求数据
|
|
136
137
|
payload = {"ctrl_env": ctrl_env}
|
|
137
138
|
|
|
138
|
-
response_data = make_request("POST", url, self.bytecloud_client.build_request_headers(site,
|
|
139
|
+
response_data = make_request("POST", url, self.bytecloud_client.build_request_headers(site, vregion), payload)
|
|
139
140
|
|
|
140
141
|
message = response_data.get("message")
|
|
141
142
|
logger.info(f"int_resources {site} {ctrl_env}, message: {message}")
|
|
142
143
|
|
|
143
144
|
return message == "ok"
|
|
144
145
|
|
|
145
|
-
def list_resources(self, site: str,
|
|
146
|
+
def list_resources(self, site: str, vregion: str, ctrl_env: str) -> list[str]:
|
|
146
147
|
"""
|
|
147
148
|
列举 CTRL 环境资源列表,连同 agent 列表
|
|
148
149
|
|
|
149
150
|
Args:
|
|
150
151
|
site: 站点名称
|
|
151
|
-
ctrl_env: 控制环境
|
|
152
152
|
vregion: 虚拟区域
|
|
153
|
+
ctrl_env: 控制环境
|
|
153
154
|
Returns:
|
|
154
155
|
list[str]: CTRL 环境资源列表
|
|
155
156
|
"""
|
|
@@ -164,7 +165,7 @@ class DFlowClient:
|
|
|
164
165
|
|
|
165
166
|
# curl_cmd = output_curl(url, headers, payload)
|
|
166
167
|
# print(f"Equivalent curl:\n{curl_cmd}")
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
try:
|
|
169
170
|
data = response_data.get("data", {})
|
|
170
171
|
items = data.get("items", [])
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from .bytecloud import ByteCloudClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TCEError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TCEErrorServiceExist(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TCEErrorClusterExist(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TCEClient:
|
|
20
|
+
"""Client for TCE deployment API."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, bytecloud_client: ByteCloudClient) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize TCE client.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
base_url: TCE API base URL
|
|
28
|
+
jwt_token: JWT token for authentication (can be fetched automatically)
|
|
29
|
+
"""
|
|
30
|
+
self.bytecloud_client = bytecloud_client
|
|
31
|
+
|
|
32
|
+
def create_service(self, site: str, ctrl_name: str, bytetree_parent_id: int) -> int | None:
|
|
33
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
34
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/deployment/service/create/"
|
|
35
|
+
|
|
36
|
+
service_info = {
|
|
37
|
+
"meta": {"name": f"bytedts.cm.{ctrl_name}", "psm": f"bytedts.cm.{ctrl_name}", "env": "prod"},
|
|
38
|
+
"auth": {
|
|
39
|
+
"galaxy_info": {"parent_id": bytetree_parent_id},
|
|
40
|
+
"approval_controls": ["reset"],
|
|
41
|
+
"iam_info": {"owners": [{"username": site_info.svc_account}, {"username": "mahang"}]},
|
|
42
|
+
"owner_name": site_info.svc_account,
|
|
43
|
+
},
|
|
44
|
+
"extra_features": {
|
|
45
|
+
"service_token": {
|
|
46
|
+
"tech_stack": {"language": "go", "framework": "custom", "platform_types_v_2": {"tce": ["http"]}},
|
|
47
|
+
"deploy": {"type": "stateless_compute", "scale": {"enable": True}, "descale": {"enable": True}},
|
|
48
|
+
"level": "P2",
|
|
49
|
+
"purpose": "convention",
|
|
50
|
+
"contain_user_personal_info": False,
|
|
51
|
+
"data_sensitivity_level": "L2",
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"build": {
|
|
55
|
+
"dependency_mode": "default",
|
|
56
|
+
"base_image": "debian.bookworm.tce_service:latest",
|
|
57
|
+
"install_package_info": [],
|
|
58
|
+
"scm_repo_info": [
|
|
59
|
+
{"name": "bytedts/dflow/dtscm", "remote_id": 387073, "main_repo": True, "path": "dts-cm"},
|
|
60
|
+
{"name": "toutiao/load", "remote_id": 667, "path": "toutiao/load"},
|
|
61
|
+
{"name": "toutiao/runtime", "remote_id": 631, "path": "toutiao/runtime"},
|
|
62
|
+
],
|
|
63
|
+
"language": "go",
|
|
64
|
+
"framework_support": "none",
|
|
65
|
+
"tech_stack": "custom",
|
|
66
|
+
},
|
|
67
|
+
"runtime": {
|
|
68
|
+
"umount_ss_conf": False,
|
|
69
|
+
"new_ports": [
|
|
70
|
+
{"intent": "primary", "port": 8088, "is_primary": True, "type": "tcp"},
|
|
71
|
+
{"intent": "other", "port": 3033, "is_primary": False, "type": "tcp"},
|
|
72
|
+
],
|
|
73
|
+
"is_runtime": False,
|
|
74
|
+
"app_path": "dts-cm/bootstrap.sh",
|
|
75
|
+
"enable_iast": False,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
payload = {"service_info": service_info}
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
response = requests.post(url, json=payload, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
83
|
+
if response.status_code != 200:
|
|
84
|
+
if "already exists" in response.text:
|
|
85
|
+
raise TCEErrorServiceExist("TCE cluster with the same name already exists")
|
|
86
|
+
|
|
87
|
+
raise TCEError(f"TCE API request failed with status {response.status_code}\n" f"Response: {response.text}")
|
|
88
|
+
|
|
89
|
+
result = response.json()
|
|
90
|
+
if result.get("code", 0) == -1:
|
|
91
|
+
raise TCEError(f"TCE API error: {result.get('error')}")
|
|
92
|
+
|
|
93
|
+
return result.get("data", {}).get("service", 0)
|
|
94
|
+
|
|
95
|
+
except requests.RequestException as e:
|
|
96
|
+
raise TCEError(f"Failed to create TCE cluster: {str(e)}") from e
|
|
97
|
+
|
|
98
|
+
def create_cluster(self, site: str, cluster_info: dict[str, Any], service: int) -> int | None:
|
|
99
|
+
"""
|
|
100
|
+
Create TCE cluster.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
cluster_info: Cluster configuration (meta, resource, runtime)
|
|
104
|
+
service: Service ID
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The created cluster ID, or None if creation failed
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
TCEError: If API call fails
|
|
111
|
+
"""
|
|
112
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
113
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/deployment/cluster/create/"
|
|
114
|
+
payload = {"cluster_info": cluster_info, "service": service}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
response = requests.post(url, json=payload, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
118
|
+
if response.status_code != 200:
|
|
119
|
+
if "a same named TCE cluster already exists" in response.text:
|
|
120
|
+
raise TCEErrorClusterExist("TCE cluster with the same name already exists")
|
|
121
|
+
|
|
122
|
+
raise TCEError(f"TCE API request failed with status {response.status_code}\n" f"Response: {response.text}")
|
|
123
|
+
|
|
124
|
+
result = response.json()
|
|
125
|
+
|
|
126
|
+
# Check for errors in response
|
|
127
|
+
if not isinstance(result, dict):
|
|
128
|
+
raise TCEError(f"Unexpected response format: {result}")
|
|
129
|
+
|
|
130
|
+
# TCE API may return error information in different formats
|
|
131
|
+
if result.get("code", 0) == -1:
|
|
132
|
+
raise TCEError(f"TCE API error: {result.get('error')}")
|
|
133
|
+
|
|
134
|
+
data = result.get("data", {})
|
|
135
|
+
create_success = data.get("deployment_type", "") == "create" and data.get("status", "") == "pending"
|
|
136
|
+
if not create_success:
|
|
137
|
+
raise TCEError(f"TCE cluster creation failed: {result}")
|
|
138
|
+
|
|
139
|
+
cluster_id = data.get("cluster_info", {}).get("meta", {}).get("id", 0)
|
|
140
|
+
# Ensure cluster_id is an integer
|
|
141
|
+
return int(cluster_id) if cluster_id else 0
|
|
142
|
+
|
|
143
|
+
except requests.RequestException as e:
|
|
144
|
+
raise TCEError(f"Failed to create TCE cluster: {str(e)}") from e
|
|
145
|
+
|
|
146
|
+
def delete_cluster(self, site: str, cluster_id: int, service_id: int, cluster_name: str) -> int | None:
|
|
147
|
+
"""
|
|
148
|
+
Delete TCE cluster (reset/destroy).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
site: Site identifier
|
|
152
|
+
cluster_id: Cluster ID to delete
|
|
153
|
+
service_id: Service ID
|
|
154
|
+
cluster_name: Cluster name
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Response data
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
TCEError: If API call fails
|
|
161
|
+
"""
|
|
162
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
163
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/deployment/cluster/reset/"
|
|
164
|
+
payload = {"cluster": cluster_id, "service": service_id}
|
|
165
|
+
|
|
166
|
+
# print(f"Deleting TCE cluster {cluster_id} for service {service_id}")
|
|
167
|
+
try:
|
|
168
|
+
response = requests.post(url, json=payload, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
169
|
+
if response.status_code != 200:
|
|
170
|
+
raise TCEError(f"TCE API request failed with status {response.status_code}\n" f"Response: {response.text}")
|
|
171
|
+
|
|
172
|
+
result = response.json()
|
|
173
|
+
if result.get("code", 0) == -1:
|
|
174
|
+
error_msg = result.get("error", "")
|
|
175
|
+
if "do not exist" in error_msg:
|
|
176
|
+
print(f"cluster {cluster_name} not exist, maybe already deleted")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
raise TCEError(f"TCE API error: {error_msg}")
|
|
180
|
+
|
|
181
|
+
data = result.get("data", {})
|
|
182
|
+
# 查找 "人工确认" step 的 id
|
|
183
|
+
steps = data.get("steps", [])
|
|
184
|
+
for step in steps:
|
|
185
|
+
if step.get("step_type") == "ConfirmStep":
|
|
186
|
+
return step.get("id")
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
except requests.RequestException as e:
|
|
191
|
+
raise TCEError(f"Failed to delete TCE cluster: {cluster_name}, msg: {str(e)}") from e
|
|
192
|
+
|
|
193
|
+
def get_cluster_info(self, site: str, cluster_id: int) -> dict[str, Any]:
|
|
194
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
195
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/clusters/{cluster_id}/"
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
response = requests.get(url, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
199
|
+
if response.status_code != 200:
|
|
200
|
+
raise TCEError(f"TCE API request failed with status {response.status_code}\n" f"Response: {response.text}")
|
|
201
|
+
|
|
202
|
+
# print(f"get_cluster_info response.text: {response.text}")
|
|
203
|
+
result = response.json()
|
|
204
|
+
if result.get("code", 0) == -1:
|
|
205
|
+
raise TCEError(f"TCE API error: {result.get('error')}")
|
|
206
|
+
|
|
207
|
+
return result.get("data", {})
|
|
208
|
+
|
|
209
|
+
except requests.RequestException as e:
|
|
210
|
+
raise TCEError(f"Failed to create TCE cluster: {str(e)}") from e
|
|
211
|
+
|
|
212
|
+
def is_cluster_running(self, site: str, cluster_id: int | None) -> bool:
|
|
213
|
+
if cluster_id is None:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
cluster_info = self.get_cluster_info(site, cluster_id)
|
|
217
|
+
return cluster_info.get("meta", {}).get("status", "unknown") == "running"
|
|
218
|
+
|
|
219
|
+
def restart_cluster(self, site: str, service_id: int, cluster_id: int) -> list[int]:
|
|
220
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
221
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/deployment/cluster/pod_exec/"
|
|
222
|
+
|
|
223
|
+
payload = {
|
|
224
|
+
"service": service_id,
|
|
225
|
+
"cluster": cluster_id,
|
|
226
|
+
"exec_command": "rebuild",
|
|
227
|
+
"exec_params": {
|
|
228
|
+
"surge_percent": 25,
|
|
229
|
+
"ready_seconds": 10,
|
|
230
|
+
"timeout": 120,
|
|
231
|
+
"tolerance_num": 3,
|
|
232
|
+
"failed_policy": "remain",
|
|
233
|
+
"need_check": True,
|
|
234
|
+
},
|
|
235
|
+
"exec_type": "rebuild",
|
|
236
|
+
"parallel_mode": True,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
response = requests.post(url, json=payload, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
241
|
+
if response.status_code != 200:
|
|
242
|
+
raise TCEError(f"TCE API request failed with status {response.status_code}\n" f"Response: {response.text}")
|
|
243
|
+
|
|
244
|
+
result = response.json()
|
|
245
|
+
if result.get("code", 0) == -1:
|
|
246
|
+
raise TCEError(f"TCE API error: {result.get('error')}")
|
|
247
|
+
|
|
248
|
+
steps = result.get("data", {}).get("steps", [])
|
|
249
|
+
step_ids = [step.get("id", 0) for step in steps]
|
|
250
|
+
return step_ids
|
|
251
|
+
|
|
252
|
+
except requests.RequestException as e:
|
|
253
|
+
raise TCEError(f"Failed to create TCE cluster: {str(e)}") from e
|
|
254
|
+
|
|
255
|
+
def confirm_step(self, site: str, step_id: int, action: str = "confirm") -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Confirm a step in TCE cluster deployment.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
step_id: Step ID
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Response data
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
TCEError: If API call fails
|
|
267
|
+
"""
|
|
268
|
+
site_info = self.bytecloud_client.get_site_config(site)
|
|
269
|
+
url = f"{site_info.endpoint}/api/v1/tce/open-apis/v1/deployment_steps/{step_id}/actions/"
|
|
270
|
+
payload = {"action": action}
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
response = requests.post(url, json=payload, headers=self.bytecloud_client.build_request_headers(site), timeout=60)
|
|
274
|
+
# print(f"Confirm step response: {response.text}")
|
|
275
|
+
|
|
276
|
+
response.raise_for_status()
|
|
277
|
+
result = response.json()
|
|
278
|
+
|
|
279
|
+
# Check for errors in response
|
|
280
|
+
if not isinstance(result, dict):
|
|
281
|
+
raise TCEError(f"Unexpected response format: {result}")
|
|
282
|
+
|
|
283
|
+
if result.get("code", 0) == -1:
|
|
284
|
+
raise TCEError(f"TCE API error: {result.get('error')}")
|
|
285
|
+
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
except requests.RequestException as e:
|
|
289
|
+
raise TCEError(f"Failed to confirm ticket, msg: {str(e)}") from e
|
|
@@ -16,11 +16,13 @@ def test_get_task_info():
|
|
|
16
16
|
info = dflow_client.get_task_info("cn", "10457709471247")
|
|
17
17
|
print(f"DFlow Info: {info}")
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
# pytest tests/test_dflow.py::test_list_resources -s
|
|
20
21
|
def test_list_resources():
|
|
21
|
-
resources = dflow_client.list_resources("cn", "
|
|
22
|
+
resources = dflow_client.list_resources("cn", "China-East", "cn_east")
|
|
22
23
|
print(f"list_resources: {json.dumps(resources, indent=2)}")
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
# pytest tests/test_dflow.py::test_get_all_ctrl_envs -s
|
|
25
27
|
def test_get_all_ctrl_envs():
|
|
26
28
|
ctrl_envs = dflow_client.get_all_ctrl_envs("cn")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|