alibaba-cloud-ops-mcp-server 0.9.25__py3-none-any.whl → 0.9.27__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.
- alibaba_cloud_ops_mcp_server/server.py +151 -3
- alibaba_cloud_ops_mcp_server/tools/application_management_tools.py +572 -41
- {alibaba_cloud_ops_mcp_server-0.9.25.dist-info → alibaba_cloud_ops_mcp_server-0.9.27.dist-info}/METADATA +15 -12
- {alibaba_cloud_ops_mcp_server-0.9.25.dist-info → alibaba_cloud_ops_mcp_server-0.9.27.dist-info}/RECORD +7 -7
- {alibaba_cloud_ops_mcp_server-0.9.25.dist-info → alibaba_cloud_ops_mcp_server-0.9.27.dist-info}/WHEEL +0 -0
- {alibaba_cloud_ops_mcp_server-0.9.25.dist-info → alibaba_cloud_ops_mcp_server-0.9.27.dist-info}/entry_points.txt +0 -0
- {alibaba_cloud_ops_mcp_server-0.9.25.dist-info → alibaba_cloud_ops_mcp_server-0.9.27.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import ast
|
|
2
5
|
|
|
3
6
|
from fastmcp import FastMCP
|
|
4
7
|
import click
|
|
@@ -43,6 +46,97 @@ SUPPORTED_SERVICES_MAP = {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def _register_tools_with_filter(mcp: FastMCP, visible_tools_set: set, visible_tools_original: dict, services: str, extra_config: str):
|
|
50
|
+
"""
|
|
51
|
+
Register tools based on visible_tools whitelist (case-insensitive).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
mcp: FastMCP instance
|
|
55
|
+
visible_tools_set: Set of allowed tool names (lowercase for case-insensitive matching)
|
|
56
|
+
visible_tools_original: Dict mapping lowercase names to original names
|
|
57
|
+
services: Services parameter for common_api_tools
|
|
58
|
+
extra_config: Extra config parameter
|
|
59
|
+
"""
|
|
60
|
+
registered_tools = set()
|
|
61
|
+
|
|
62
|
+
# 1. Register common_api_tools if services is specified
|
|
63
|
+
if services:
|
|
64
|
+
service_keys = [s.strip().lower() for s in services.split(",")]
|
|
65
|
+
service_list = [(key, SUPPORTED_SERVICES_MAP.get(key, key)) for key in service_keys]
|
|
66
|
+
set_custom_service_list(service_list)
|
|
67
|
+
for tool in common_api_tools.tools:
|
|
68
|
+
if tool.__name__.lower() in visible_tools_set:
|
|
69
|
+
mcp.tool(tool)
|
|
70
|
+
registered_tools.add(tool.__name__.lower())
|
|
71
|
+
|
|
72
|
+
# 2. Register static tools from all modules
|
|
73
|
+
static_tool_modules = [
|
|
74
|
+
oos_tools,
|
|
75
|
+
application_management_tools,
|
|
76
|
+
cms_tools,
|
|
77
|
+
oss_tools,
|
|
78
|
+
local_tools
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
for tool_module in static_tool_modules:
|
|
82
|
+
for tool in tool_module.tools:
|
|
83
|
+
if tool.__name__.lower() in visible_tools_set:
|
|
84
|
+
mcp.tool(tool)
|
|
85
|
+
registered_tools.add(tool.__name__.lower())
|
|
86
|
+
|
|
87
|
+
# 3. Handle dynamic API tools
|
|
88
|
+
# Parse tool names in visible_tools that follow SERVICE_API format
|
|
89
|
+
# Use original names to preserve API case (important for Alibaba Cloud APIs)
|
|
90
|
+
dynamic_tools_config = {}
|
|
91
|
+
|
|
92
|
+
for tool_name_lower in visible_tools_set:
|
|
93
|
+
if '_' in tool_name_lower and tool_name_lower not in registered_tools:
|
|
94
|
+
# Get original name to preserve API case
|
|
95
|
+
original_name = visible_tools_original.get(tool_name_lower, tool_name_lower)
|
|
96
|
+
parts = original_name.split('_', 1)
|
|
97
|
+
if len(parts) == 2:
|
|
98
|
+
service_code = parts[0].lower()
|
|
99
|
+
api_name = parts[1] # Preserve original case
|
|
100
|
+
if service_code not in dynamic_tools_config:
|
|
101
|
+
dynamic_tools_config[service_code] = []
|
|
102
|
+
if api_name not in dynamic_tools_config[service_code]:
|
|
103
|
+
dynamic_tools_config[service_code].append(api_name)
|
|
104
|
+
|
|
105
|
+
# 4. Merge with extra_config (only tools in visible_tools whitelist)
|
|
106
|
+
if extra_config:
|
|
107
|
+
try:
|
|
108
|
+
try:
|
|
109
|
+
extra = json.loads(extra_config)
|
|
110
|
+
except json.JSONDecodeError:
|
|
111
|
+
extra = ast.literal_eval(extra_config)
|
|
112
|
+
|
|
113
|
+
for service_code, apis in extra.items():
|
|
114
|
+
for api in apis:
|
|
115
|
+
tool_name_lower = f"{service_code.lower()}_{api.lower()}"
|
|
116
|
+
if tool_name_lower in visible_tools_set:
|
|
117
|
+
if service_code not in dynamic_tools_config:
|
|
118
|
+
dynamic_tools_config[service_code] = []
|
|
119
|
+
if api not in dynamic_tools_config[service_code]:
|
|
120
|
+
dynamic_tools_config[service_code].append(api)
|
|
121
|
+
logger.info(f'Merged extra config with visible tools filter: {dynamic_tools_config}')
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f'Failed to parse extra-config: {e}')
|
|
124
|
+
|
|
125
|
+
# 5. Create dynamic API tools
|
|
126
|
+
if dynamic_tools_config:
|
|
127
|
+
api_tools.create_api_tools(mcp, dynamic_tools_config)
|
|
128
|
+
for service_code, apis in dynamic_tools_config.items():
|
|
129
|
+
for api in apis:
|
|
130
|
+
registered_tools.add(f"{service_code.lower()}_{api.lower()}")
|
|
131
|
+
|
|
132
|
+
# 6. Log results
|
|
133
|
+
unregistered_tools = visible_tools_set - registered_tools
|
|
134
|
+
if unregistered_tools:
|
|
135
|
+
logger.warning(f"The following tools were not found and not registered: {unregistered_tools}")
|
|
136
|
+
|
|
137
|
+
logger.info(f"Visible tools mode: registered {len(registered_tools)} tools: {registered_tools}")
|
|
138
|
+
|
|
139
|
+
|
|
46
140
|
@click.command()
|
|
47
141
|
@click.option(
|
|
48
142
|
"--transport",
|
|
@@ -86,7 +180,19 @@ SUPPORTED_SERVICES_MAP = {
|
|
|
86
180
|
default=False,
|
|
87
181
|
help="Enable code deploy mode, only load 6 specific tools: OOS_CodeDeploy, OOS_GetDeployStatus, OOS_GetLastDeploymentInfo, LOCAL_ListDirectory, LOCAL_RunShellScript, LOCAL_AnalyzeDeployStack",
|
|
88
182
|
)
|
|
89
|
-
|
|
183
|
+
@click.option(
|
|
184
|
+
"--extra-config",
|
|
185
|
+
type=str,
|
|
186
|
+
default=None,
|
|
187
|
+
help="Add extra services and APIs to config, e.g., \"{'sls': ['GetProject', 'ListProject'], 'ecs': ['StartInstance']}\"",
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--visible-tools",
|
|
191
|
+
type=str,
|
|
192
|
+
default=None,
|
|
193
|
+
help="Comma-separated list of tool names to make visible (whitelist mode). Only these tools will be registered when specified, e.g., 'OOS_RunCommand,ECS_DescribeInstances,LOCAL_ListDirectory'",
|
|
194
|
+
)
|
|
195
|
+
def main(transport: str, port: int, host: str, services: str, headers_credential_only: bool, env: str, code_deploy: bool, extra_config: str, visible_tools: str):
|
|
90
196
|
_setup_logging()
|
|
91
197
|
# Create an MCP server
|
|
92
198
|
mcp = FastMCP(
|
|
@@ -158,6 +264,12 @@ def main(transport: str, port: int, host: str, services: str, headers_credential
|
|
|
158
264
|
settings.headers_credential_only = headers_credential_only
|
|
159
265
|
if env:
|
|
160
266
|
settings.env = env
|
|
267
|
+
|
|
268
|
+
# Handle mutual exclusivity between code_deploy and visible_tools
|
|
269
|
+
if code_deploy and visible_tools:
|
|
270
|
+
logger.warning("--code-deploy and --visible-tools are mutually exclusive. Using --code-deploy mode.")
|
|
271
|
+
visible_tools = None
|
|
272
|
+
|
|
161
273
|
if code_deploy:
|
|
162
274
|
# Code deploy mode: only load 6 specific tools
|
|
163
275
|
code_deploy_tools = {
|
|
@@ -179,7 +291,21 @@ def main(transport: str, port: int, host: str, services: str, headers_credential
|
|
|
179
291
|
for tool in local_tools.tools:
|
|
180
292
|
if tool.__name__ in code_deploy_tools:
|
|
181
293
|
mcp.tool(tool)
|
|
182
|
-
|
|
294
|
+
elif visible_tools:
|
|
295
|
+
# Visible tools mode: only load tools in whitelist (case-insensitive)
|
|
296
|
+
# Build both lowercase set and original name mapping
|
|
297
|
+
visible_tools_list = [tool.strip() for tool in visible_tools.split(",") if tool.strip()]
|
|
298
|
+
visible_tools_set = set(t.lower() for t in visible_tools_list)
|
|
299
|
+
visible_tools_original = {t.lower(): t for t in visible_tools_list}
|
|
300
|
+
if visible_tools_set:
|
|
301
|
+
logger.info(f"Visible tools mode enabled (case-insensitive). Allowed tools: {visible_tools_set}")
|
|
302
|
+
_register_tools_with_filter(mcp, visible_tools_set, visible_tools_original, services, extra_config)
|
|
303
|
+
else:
|
|
304
|
+
logger.warning("--visible-tools is empty, falling back to normal mode.")
|
|
305
|
+
# Fall through to normal mode by setting a flag
|
|
306
|
+
visible_tools = None
|
|
307
|
+
|
|
308
|
+
if not code_deploy and not visible_tools:
|
|
183
309
|
# Normal mode: load all tools
|
|
184
310
|
if services:
|
|
185
311
|
service_keys = [s.strip().lower() for s in services.split(",")]
|
|
@@ -197,7 +323,29 @@ def main(transport: str, port: int, host: str, services: str, headers_credential
|
|
|
197
323
|
mcp.tool(tool)
|
|
198
324
|
for tool in oss_tools.tools:
|
|
199
325
|
mcp.tool(tool)
|
|
200
|
-
|
|
326
|
+
# Merge extra_config into the existing config
|
|
327
|
+
merged_config = copy.deepcopy(config)
|
|
328
|
+
if extra_config:
|
|
329
|
+
try:
|
|
330
|
+
# Try JSON first, then fallback to ast.literal_eval for single quotes
|
|
331
|
+
try:
|
|
332
|
+
extra = json.loads(extra_config)
|
|
333
|
+
except json.JSONDecodeError:
|
|
334
|
+
extra = ast.literal_eval(extra_config)
|
|
335
|
+
for service_code, apis in extra.items():
|
|
336
|
+
if service_code in merged_config:
|
|
337
|
+
# Add new APIs to existing service (avoid duplicates)
|
|
338
|
+
existing_apis = set(merged_config[service_code])
|
|
339
|
+
for api in apis:
|
|
340
|
+
if api not in existing_apis:
|
|
341
|
+
merged_config[service_code].append(api)
|
|
342
|
+
else:
|
|
343
|
+
# Add new service with its APIs
|
|
344
|
+
merged_config[service_code] = apis
|
|
345
|
+
logger.info(f'Merged extra config: {extra}')
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f'Failed to parse extra-config: {e}')
|
|
348
|
+
api_tools.create_api_tools(mcp, merged_config)
|
|
201
349
|
for tool in local_tools.tools:
|
|
202
350
|
mcp.tool(tool)
|
|
203
351
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import logging
|
|
3
|
+
import tarfile
|
|
4
|
+
import zipfile
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
3
7
|
|
|
4
8
|
from alibaba_cloud_ops_mcp_server.tools.api_tools import _tools_api_call
|
|
5
9
|
from pathlib import Path
|
|
6
10
|
import alibabacloud_oss_v2 as oss
|
|
7
11
|
from pydantic import Field
|
|
8
|
-
from typing import Optional, Tuple, List
|
|
12
|
+
from typing import Optional, Tuple, List, Dict, Set
|
|
9
13
|
import json
|
|
10
14
|
import time
|
|
11
15
|
from alibabacloud_oos20190601.client import Client as oos20190601Client
|
|
@@ -48,33 +52,13 @@ def OOS_CodeDeploy(
|
|
|
48
52
|
deploy_region_id: str = Field(description='Region ID for deployment'),
|
|
49
53
|
application_group_name: str = Field(description='name of the application group'),
|
|
50
54
|
object_name: str = Field(description='OSS object name'),
|
|
51
|
-
file_path: str = Field(description='Local file path to upload. If the file is not in '
|
|
52
|
-
'.code_deploy/release directory, it will be copied there.'),
|
|
53
|
-
application_start: str = Field(
|
|
54
|
-
description='Application start command script. IMPORTANT: If the uploaded artifact '
|
|
55
|
-
'is a tar archive or compressed package (e.g., .tar, .tar.gz, .zip), '
|
|
56
|
-
'you MUST first extract it and navigate into the corresponding directory'
|
|
57
|
-
' before executing the start command. The start command must correspond '
|
|
58
|
-
'to the actual structure of the extracted artifact. For example, if you '
|
|
59
|
-
'upload a tar.gz file containing a Java application, first extract it '
|
|
60
|
-
'with "tar -xzf <filename>.tar.gz", then cd into the extracted '
|
|
61
|
-
'directory, and then run the start command (e.g., "java -jar app.jar" '
|
|
62
|
-
'or "./start.sh"). Ensure the start command matches the actual '
|
|
63
|
-
'executable or script in the extracted artifact to avoid deployment '
|
|
64
|
-
'failures. Do not blindly use the `cd` command; always verify that the corresponding file '
|
|
65
|
-
'and path exist before using it.'),
|
|
66
|
-
application_stop: str = Field(description='Application stop command script, Defensive stop command - checks if '
|
|
67
|
-
'the service exists and if the CD path exists, preventing errors '
|
|
68
|
-
'caused by blindly using `cd` or due to non-existent commands.'),
|
|
55
|
+
file_path: str = Field(description='Local file path to upload. If the file is not in .code_deploy/release directory, it will be copied there.'),
|
|
69
56
|
deploy_language: str = Field(description='Deploy language, like:docker, java, python, nodejs, golang'),
|
|
70
57
|
port: int = Field(description='Application listening port'),
|
|
71
|
-
project_path: Optional[str] = Field(description='Root path of the project. The .code_deploy '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
instance_ids: list = Field(description='AlibabaCloud ECS instance ID List. If empty or not provided, user '
|
|
76
|
-
'will be prompted to create ECS instances.', default=None)
|
|
77
|
-
|
|
58
|
+
project_path: Optional[str] = Field(description='Root path of the project. The .code_deploy directory will be created in this path. If not provided, will try to infer from file_path or use current working directory.'),
|
|
59
|
+
application_start: Optional[str] = Field(default=None, description='**OPTIONAL - DO NOT PROVIDE UNLESS USER EXPLICITLY REQUIRES IT** Application start command script. In normal cases, you should NOT provide this parameter. The system will automatically generate the start command using rule engine based on deployment file analysis and deploy_language. Only provide this parameter when: (1) User explicitly specifies a custom start command, or (2) User provides specific instructions about the start command. The auto-generated command includes defensive checks for file/path existence and proper archive extraction. If you must provide a custom command, ensure it includes defensive checks like "[ -f file ] && command" and handles archive extraction properly.'),
|
|
60
|
+
application_stop: Optional[str] = Field( default=None, description='**OPTIONAL - DO NOT PROVIDE UNLESS USER EXPLICITLY REQUIRES IT** Application stop command script. In normal cases, you should NOT provide this parameter. The system will automatically generate the stop command using rule engine based on deploy_language. Only provide this parameter when: (1) User explicitly specifies a custom stop command, or (2) User provides specific instructions about the stop command. The auto-generated command includes defensive checks and proper process termination.'),
|
|
61
|
+
instance_ids: list = Field(description='AlibabaCloud ECS instance ID List. If empty or not provided, user will be prompted to create ECS instances.', default=None)
|
|
78
62
|
):
|
|
79
63
|
"""
|
|
80
64
|
将应用部署到阿里云ECS实例。使用阿里云OOS(运维编排服务)的CodeDeploy功能实现自动化部署。
|
|
@@ -85,31 +69,41 @@ def OOS_CodeDeploy(
|
|
|
85
69
|
2. **构建部署产物**:执行构建命令生成压缩包(tar.gz、zip等),保存到 `.code_deploy/release` 目录
|
|
86
70
|
3. **准备ECS实例**:确保目标ECS实例已创建,获取实例ID列表
|
|
87
71
|
|
|
88
|
-
##
|
|
72
|
+
## ⚠️ 重要提示:启动和停止命令参数
|
|
73
|
+
|
|
74
|
+
**正常情况下,不需要提供 application_start 和 application_stop 参数!**
|
|
75
|
+
|
|
76
|
+
- 系统会自动通过规则引擎分析部署文件和 deploy_language,自动生成启动和停止命令
|
|
77
|
+
- 自动生成的命令包含所有必要的防御性检查(文件存在性、路径存在性等)
|
|
78
|
+
- 自动生成的命令正确处理压缩包解压、目录切换、后台运行、日志重定向等
|
|
79
|
+
- 对于 Node.js 应用,自动生成的命令会先执行 `npm install` 安装依赖
|
|
80
|
+
|
|
81
|
+
**只有在以下情况下才需要手动提供这两个参数:**
|
|
82
|
+
1. 用户明确指定了自定义的启动/停止命令
|
|
83
|
+
2. 用户提供了关于启动/停止命令的特殊要求或说明
|
|
84
|
+
|
|
85
|
+
**调用建议:直接调用工具,不传 application_start 和 application_stop 参数即可。**
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
## 自动生成的命令规范(供参考,了解即可)
|
|
88
|
+
|
|
89
|
+
### 1. 防御性命令设计
|
|
90
|
+
自动生成的命令包含存在性检查,避免因路径/文件/命令不存在导致失败:
|
|
92
91
|
- 压缩包:先检查文件存在再解压 `[ -f app.tar.gz ] && tar -xzf app.tar.gz || exit 1`
|
|
93
92
|
- 可执行文件:检查文件存在再执行 `[ -f start.sh ] && chmod +x start.sh && ./start.sh || exit 1`
|
|
94
93
|
- 命令可用性:检查命令是否存在 `command -v npm >/dev/null 2>&1 || exit 1`
|
|
95
|
-
-
|
|
94
|
+
- 目录切换:先验证路径存在再切换 `[ -d dir ] && cd dir || exit 1`
|
|
96
95
|
|
|
97
|
-
### 2.
|
|
98
|
-
|
|
96
|
+
### 2. 压缩包处理规范
|
|
97
|
+
如果产物是压缩包,自动生成的命令会先解压:
|
|
99
98
|
- 使用非交互式命令:`tar -xzf`、`unzip -o`(自动覆盖,无需确认)
|
|
100
99
|
- 解压后执行启动命令,确保路径对应
|
|
101
|
-
- 示例:`tar -xzf app.tar.gz && nohup java -jar app.jar > /root/app.log 2>&1 &`
|
|
100
|
+
- 示例:`tar -xzf app.tar.gz && [ -d app ] && cd app && nohup java -jar app.jar > /root/app.log 2>&1 &`
|
|
102
101
|
|
|
103
|
-
### 3.
|
|
104
|
-
|
|
102
|
+
### 3. 后台运行与日志
|
|
103
|
+
自动生成的启动命令使用后台运行并重定向日志:
|
|
105
104
|
- 格式:`nohup <command> > /root/app.log 2>&1 &`
|
|
106
105
|
- 说明:nohup保持后台运行,`>` 重定向标准输出,`2>&1` 合并错误输出,`&` 后台执行
|
|
107
106
|
|
|
108
|
-
### 4. 停止命令规范
|
|
109
|
-
停止命令需检查服务/进程是否存在:
|
|
110
|
-
- systemctl服务:`systemctl list-units | grep -q "service" && systemctl stop service`
|
|
111
|
-
- 进程名:`pkill -f "process_pattern" || true`
|
|
112
|
-
|
|
113
107
|
## 注意事项
|
|
114
108
|
|
|
115
109
|
- 应用和应用分组会自动检查,已存在则跳过创建
|
|
@@ -238,13 +232,60 @@ def OOS_CodeDeploy(
|
|
|
238
232
|
file_path = str(release_path_resolved)
|
|
239
233
|
else:
|
|
240
234
|
logger.info(f"[code_deploy] File already in release directory: {file_path}")
|
|
235
|
+
|
|
236
|
+
# 如果未提供启动/停止命令,尝试通过规则引擎生成
|
|
237
|
+
if not application_start or not application_stop:
|
|
238
|
+
logger.info(f"[code_deploy] Attempting to auto-generate commands using rule engine. "
|
|
239
|
+
f"Provided: start={application_start is not None}, stop={application_stop is not None}")
|
|
240
|
+
|
|
241
|
+
generated_start, generated_stop = _generate_start_stop_commands_by_rules(
|
|
242
|
+
file_path, deploy_language, name, port
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if not application_start and generated_start:
|
|
246
|
+
application_start = generated_start
|
|
247
|
+
logger.info(f"[code_deploy] Auto-generated start command successfully")
|
|
248
|
+
elif not application_start:
|
|
249
|
+
logger.warning(f"[code_deploy] Failed to generate start command automatically, manual input required")
|
|
250
|
+
return {
|
|
251
|
+
'error': 'START_COMMAND_REQUIRED',
|
|
252
|
+
'message': '无法通过规则引擎自动生成启动命令,请手动提供 application_start 参数并再次调用OOS_CodeDeploy',
|
|
253
|
+
'file_path': file_path,
|
|
254
|
+
'deploy_language': deploy_language,
|
|
255
|
+
'instructions': f'''
|
|
256
|
+
## 启动命令生成失败
|
|
257
|
+
|
|
258
|
+
系统尝试通过规则引擎自动生成启动命令,但未能成功识别。
|
|
259
|
+
|
|
260
|
+
**文件路径**: {file_path}
|
|
261
|
+
**部署语言**: {deploy_language}
|
|
262
|
+
|
|
263
|
+
请根据以下信息手动提供启动命令:
|
|
264
|
+
1. 如果文件是压缩包(tar.gz、zip等),需要先解压
|
|
265
|
+
2. 启动命令必须包含防御性检查(检查文件/路径是否存在)
|
|
266
|
+
3. 启动命令必须使用后台运行并重定向日志:`nohup <command> > /root/app.log 2>&1 &`
|
|
267
|
+
|
|
268
|
+
示例:
|
|
269
|
+
- Java: `[ -f app.tar.gz ] && tar -xzf app.tar.gz && [ -f app/app.jar ] && nohup java -jar app/app.jar > /root/app.log 2>&1 &`
|
|
270
|
+
- Python: `[ -f app.tar.gz ] && tar -xzf app.tar.gz && [ -f app/app.py ] && nohup python app/app.py > /root/app.log 2>&1 &`
|
|
271
|
+
- Node.js: `[ -f app.tar.gz ] && tar -xzf app.tar.gz && [ -f app/package.json ] && nohup npm start > /root/app.log 2>&1 &`
|
|
272
|
+
'''
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if not application_stop and generated_stop:
|
|
276
|
+
application_stop = generated_stop
|
|
277
|
+
logger.info(f"[code_deploy] Auto-generated stop command successfully")
|
|
278
|
+
|
|
279
|
+
logger.info(f"[code_deploy] Deployment commands ready - start: {bool(application_start)}, stop: {bool(application_stop)}")
|
|
280
|
+
|
|
241
281
|
region_id_oss = 'cn-hangzhou'
|
|
242
282
|
is_internal_oss = True if deploy_region_id.lower() == 'cn-hangzhou' else False
|
|
243
283
|
# Log input parameters
|
|
244
284
|
logger.info(f"[code_deploy] Input parameters: name={name}, deploy_region_id={deploy_region_id}, "
|
|
245
285
|
f"application_group_name={application_group_name}, instance_ids={instance_ids}, "
|
|
246
286
|
f"region_id_oss={region_id_oss}, object_name={object_name}, "
|
|
247
|
-
f"is_internal_oss={is_internal_oss}, port={port}"
|
|
287
|
+
f"is_internal_oss={is_internal_oss}, port={port}, "
|
|
288
|
+
f"application_start={application_start}, application_stop={application_stop}")
|
|
248
289
|
|
|
249
290
|
# Upload file to OSS
|
|
250
291
|
try:
|
|
@@ -808,3 +849,493 @@ def _create_revision_deploy_parameters():
|
|
|
808
849
|
"Mode": "FailurePause"
|
|
809
850
|
}
|
|
810
851
|
}
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _extract_top_level_dir(members: List[str]) -> Optional[str]:
|
|
855
|
+
"""
|
|
856
|
+
从压缩包成员列表中提取顶级目录名
|
|
857
|
+
|
|
858
|
+
如果所有文件都在同一个顶级目录下,返回该目录名;否则返回 None
|
|
859
|
+
"""
|
|
860
|
+
if not members:
|
|
861
|
+
return None
|
|
862
|
+
|
|
863
|
+
top_level = set()
|
|
864
|
+
for member in members:
|
|
865
|
+
parts = member.split('/')
|
|
866
|
+
if parts[0]:
|
|
867
|
+
top_level.add(parts[0])
|
|
868
|
+
|
|
869
|
+
if len(top_level) == 1:
|
|
870
|
+
return list(top_level)[0]
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _analyze_deployment_file(file_path: str) -> Dict:
|
|
875
|
+
"""
|
|
876
|
+
分析部署文件,返回文件类型和内容列表
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
{
|
|
880
|
+
'file_type': 'archive' | 'directory' | 'file',
|
|
881
|
+
'archive_type': 'tar.gz' | 'tar' | 'zip' | None,
|
|
882
|
+
'file_name': str,
|
|
883
|
+
'files_in_archive': List[str], # 压缩包内的文件列表
|
|
884
|
+
'extracted_dir_name': Optional[str], # 解压后的目录名(如果有)
|
|
885
|
+
}
|
|
886
|
+
"""
|
|
887
|
+
file_path_obj = Path(file_path)
|
|
888
|
+
|
|
889
|
+
if not file_path_obj.exists():
|
|
890
|
+
logger.warning(f"[_analyze_deployment_file] File does not exist: {file_path}")
|
|
891
|
+
return {'file_type': 'unknown', 'file_name': file_path_obj.name}
|
|
892
|
+
|
|
893
|
+
result = {
|
|
894
|
+
'file_type': 'file',
|
|
895
|
+
'archive_type': None,
|
|
896
|
+
'file_name': file_path_obj.name,
|
|
897
|
+
'files_in_archive': [],
|
|
898
|
+
'extracted_dir_name': None,
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
file_name_lower = file_path_obj.name.lower()
|
|
902
|
+
|
|
903
|
+
# 处理 tar.gz 和 tar 文件
|
|
904
|
+
if file_name_lower.endswith('.tar.gz') or file_name_lower.endswith('.tgz'):
|
|
905
|
+
result['file_type'] = 'archive'
|
|
906
|
+
result['archive_type'] = 'tar.gz'
|
|
907
|
+
try:
|
|
908
|
+
with tarfile.open(file_path, 'r:gz') as tar:
|
|
909
|
+
members = tar.getnames()
|
|
910
|
+
result['files_in_archive'] = members
|
|
911
|
+
result['extracted_dir_name'] = _extract_top_level_dir(members)
|
|
912
|
+
logger.info(f"[_analyze_deployment_file] Analyzed tar.gz archive: {len(members)} files, "
|
|
913
|
+
f"extracted_dir: {result['extracted_dir_name']}")
|
|
914
|
+
except Exception as e:
|
|
915
|
+
logger.warning(f"[_analyze_deployment_file] Failed to read tar.gz: {e}")
|
|
916
|
+
|
|
917
|
+
elif file_name_lower.endswith('.tar'):
|
|
918
|
+
result['file_type'] = 'archive'
|
|
919
|
+
result['archive_type'] = 'tar'
|
|
920
|
+
try:
|
|
921
|
+
with tarfile.open(file_path, 'r') as tar:
|
|
922
|
+
members = tar.getnames()
|
|
923
|
+
result['files_in_archive'] = members
|
|
924
|
+
result['extracted_dir_name'] = _extract_top_level_dir(members)
|
|
925
|
+
logger.info(f"[_analyze_deployment_file] Analyzed tar archive: {len(members)} files, "
|
|
926
|
+
f"extracted_dir: {result['extracted_dir_name']}")
|
|
927
|
+
except Exception as e:
|
|
928
|
+
logger.warning(f"[_analyze_deployment_file] Failed to read tar: {e}")
|
|
929
|
+
|
|
930
|
+
elif file_name_lower.endswith('.zip'):
|
|
931
|
+
result['file_type'] = 'archive'
|
|
932
|
+
result['archive_type'] = 'zip'
|
|
933
|
+
try:
|
|
934
|
+
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
|
935
|
+
members = zip_ref.namelist()
|
|
936
|
+
result['files_in_archive'] = members
|
|
937
|
+
result['extracted_dir_name'] = _extract_top_level_dir(members)
|
|
938
|
+
logger.info(f"[_analyze_deployment_file] Analyzed zip archive: {len(members)} files, "
|
|
939
|
+
f"extracted_dir: {result['extracted_dir_name']}")
|
|
940
|
+
except Exception as e:
|
|
941
|
+
logger.warning(f"[_analyze_deployment_file] Failed to read zip: {e}")
|
|
942
|
+
|
|
943
|
+
return result
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _find_executable_files(files_list: List[str], deploy_language: str) -> Dict[str, List[str]]:
|
|
947
|
+
"""
|
|
948
|
+
在文件列表中查找可执行文件
|
|
949
|
+
|
|
950
|
+
Returns:
|
|
951
|
+
{
|
|
952
|
+
'jar_files': List[str],
|
|
953
|
+
'py_files': List[str],
|
|
954
|
+
'js_files': List[str],
|
|
955
|
+
'go_binaries': List[str],
|
|
956
|
+
'shell_scripts': List[str],
|
|
957
|
+
'package_json': Optional[str],
|
|
958
|
+
'requirements_txt': Optional[str],
|
|
959
|
+
'dockerfile': Optional[str],
|
|
960
|
+
}
|
|
961
|
+
"""
|
|
962
|
+
result = {
|
|
963
|
+
'jar_files': [],
|
|
964
|
+
'py_files': [],
|
|
965
|
+
'js_files': [],
|
|
966
|
+
'go_binaries': [],
|
|
967
|
+
'shell_scripts': [],
|
|
968
|
+
'package_json': None,
|
|
969
|
+
'requirements_txt': None,
|
|
970
|
+
'dockerfile': None,
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
for file_path in files_list:
|
|
974
|
+
file_name = file_path.split('/')[-1].lower()
|
|
975
|
+
|
|
976
|
+
# Java
|
|
977
|
+
if file_name.endswith('.jar'):
|
|
978
|
+
result['jar_files'].append(file_path)
|
|
979
|
+
# Python
|
|
980
|
+
elif file_name.endswith('.py'):
|
|
981
|
+
result['py_files'].append(file_path)
|
|
982
|
+
# Node.js
|
|
983
|
+
elif file_name == 'package.json':
|
|
984
|
+
result['package_json'] = file_path
|
|
985
|
+
elif file_name.endswith('.js'):
|
|
986
|
+
result['js_files'].append(file_path)
|
|
987
|
+
# Go - 只在 golang 语言时查找潜在的二进制文件
|
|
988
|
+
# 排除常见的非二进制文件(文档、配置等)
|
|
989
|
+
elif deploy_language == 'golang':
|
|
990
|
+
# 排除列表:常见的非二进制文件名
|
|
991
|
+
non_binary_names = {
|
|
992
|
+
'readme', 'license', 'makefile', 'dockerfile', 'changelog',
|
|
993
|
+
'contributing', 'authors', 'version', 'manifest', 'config',
|
|
994
|
+
'gitignore', 'dockerignore', 'editorconfig', 'env'
|
|
995
|
+
}
|
|
996
|
+
file_name_no_ext = file_name.split('.')[0] if '.' in file_name else file_name
|
|
997
|
+
# 检查是否是潜在的 Go 二进制:没有扩展名(或只有一个点),且不在排除列表中
|
|
998
|
+
has_extension = '.' in file_name and not file_name.startswith('.')
|
|
999
|
+
if not has_extension and file_name_no_ext.lower() not in non_binary_names:
|
|
1000
|
+
result['go_binaries'].append(file_path)
|
|
1001
|
+
# Shell scripts
|
|
1002
|
+
elif file_name.endswith('.sh'):
|
|
1003
|
+
result['shell_scripts'].append(file_path)
|
|
1004
|
+
# Python requirements
|
|
1005
|
+
elif file_name == 'requirements.txt':
|
|
1006
|
+
result['requirements_txt'] = file_path
|
|
1007
|
+
# Dockerfile
|
|
1008
|
+
elif file_name == 'dockerfile' and not file_path.lower().endswith('.dockerignore'):
|
|
1009
|
+
result['dockerfile'] = file_path
|
|
1010
|
+
|
|
1011
|
+
logger.info(f"[_find_executable_files] Found executable files for {deploy_language}: "
|
|
1012
|
+
f"jars={len(result['jar_files'])}, python={len(result['py_files'])}, "
|
|
1013
|
+
f"js={len(result['js_files'])}, go_bins={len(result['go_binaries'])}, "
|
|
1014
|
+
f"shells={len(result['shell_scripts'])}")
|
|
1015
|
+
|
|
1016
|
+
return result
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _generate_start_command_by_rules(
|
|
1020
|
+
file_path: str,
|
|
1021
|
+
deploy_language: str,
|
|
1022
|
+
file_analysis: Dict,
|
|
1023
|
+
extracted_dir_name: Optional[str] = None,
|
|
1024
|
+
application_name: Optional[str] = None,
|
|
1025
|
+
port: Optional[int] = None
|
|
1026
|
+
) -> Optional[str]:
|
|
1027
|
+
"""
|
|
1028
|
+
根据规则生成启动命令
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
生成的启动命令,如果无法通过规则生成则返回 None
|
|
1032
|
+
"""
|
|
1033
|
+
file_name = Path(file_path).name
|
|
1034
|
+
file_name_lower = file_name.lower()
|
|
1035
|
+
|
|
1036
|
+
# 判断是否需要解压
|
|
1037
|
+
is_archive = file_analysis['file_type'] == 'archive'
|
|
1038
|
+
extract_cmd = ""
|
|
1039
|
+
work_dir = ""
|
|
1040
|
+
|
|
1041
|
+
if is_archive:
|
|
1042
|
+
archive_type = file_analysis['archive_type']
|
|
1043
|
+
if archive_type == 'tar.gz':
|
|
1044
|
+
extract_cmd = f"[ -f {file_name} ] && tar -xzf {file_name} || exit 1"
|
|
1045
|
+
elif archive_type == 'tar':
|
|
1046
|
+
extract_cmd = f"[ -f {file_name} ] && tar -xf {file_name} || exit 1"
|
|
1047
|
+
elif archive_type == 'zip':
|
|
1048
|
+
extract_cmd = f"[ -f {file_name} ] && unzip -o {file_name} || exit 1"
|
|
1049
|
+
|
|
1050
|
+
# 如果有明确的解压目录,使用它
|
|
1051
|
+
if extracted_dir_name:
|
|
1052
|
+
work_dir = f" && [ -d {extracted_dir_name} ] && cd {extracted_dir_name} || exit 1"
|
|
1053
|
+
|
|
1054
|
+
# 根据语言类型生成命令
|
|
1055
|
+
files_in_archive = file_analysis.get('files_in_archive', [])
|
|
1056
|
+
executable_files = _find_executable_files(files_in_archive, deploy_language)
|
|
1057
|
+
|
|
1058
|
+
start_cmd = ""
|
|
1059
|
+
|
|
1060
|
+
if deploy_language == 'java':
|
|
1061
|
+
# Java 应用:查找 jar 文件
|
|
1062
|
+
jar_files = executable_files['jar_files']
|
|
1063
|
+
if jar_files:
|
|
1064
|
+
# 使用第一个找到的 jar 文件
|
|
1065
|
+
jar_file = jar_files[0].split('/')[-1] # 只取文件名
|
|
1066
|
+
if is_archive and extracted_dir_name:
|
|
1067
|
+
jar_file = f"{extracted_dir_name}/{jar_file}"
|
|
1068
|
+
elif is_archive:
|
|
1069
|
+
jar_file = jar_file # 假设解压到当前目录
|
|
1070
|
+
start_cmd = f"[ -f {jar_file} ] && nohup java -jar {jar_file} > /root/app.log 2>&1 &"
|
|
1071
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Java start command for {jar_file}")
|
|
1072
|
+
else:
|
|
1073
|
+
# 尝试第一个常见的 jar 文件名(带防御性检查,如果不存在会跳过)
|
|
1074
|
+
common_jar_names = ['app.jar', 'application.jar', 'main.jar', 'server.jar']
|
|
1075
|
+
jar_name = common_jar_names[0] # 使用第一个作为默认
|
|
1076
|
+
if is_archive and extracted_dir_name:
|
|
1077
|
+
test_path = f"{extracted_dir_name}/{jar_name}"
|
|
1078
|
+
else:
|
|
1079
|
+
test_path = jar_name
|
|
1080
|
+
# 命令已包含防御性检查,如果文件不存在会失败
|
|
1081
|
+
start_cmd = f"[ -f {test_path} ] && nohup java -jar {test_path} > /root/app.log 2>&1 &"
|
|
1082
|
+
|
|
1083
|
+
elif deploy_language == 'python':
|
|
1084
|
+
# Python 应用:查找 py 文件或 requirements.txt
|
|
1085
|
+
py_files = executable_files['py_files']
|
|
1086
|
+
if py_files:
|
|
1087
|
+
# 优先查找 main.py, app.py, run.py, server.py
|
|
1088
|
+
preferred_names = ['main.py', 'app.py', 'run.py', 'server.py', 'application.py']
|
|
1089
|
+
py_file = None
|
|
1090
|
+
for preferred in preferred_names:
|
|
1091
|
+
for py_path in py_files:
|
|
1092
|
+
if py_path.endswith(preferred):
|
|
1093
|
+
py_file = py_path.split('/')[-1]
|
|
1094
|
+
break
|
|
1095
|
+
if py_file:
|
|
1096
|
+
break
|
|
1097
|
+
|
|
1098
|
+
if not py_file and py_files:
|
|
1099
|
+
py_file = py_files[0].split('/')[-1]
|
|
1100
|
+
|
|
1101
|
+
if py_file:
|
|
1102
|
+
if is_archive and extracted_dir_name:
|
|
1103
|
+
py_file = f"{extracted_dir_name}/{py_file}"
|
|
1104
|
+
start_cmd = f"[ -f {py_file} ] && nohup python {py_file} > /root/app.log 2>&1 &"
|
|
1105
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Python start command for {py_file}")
|
|
1106
|
+
else:
|
|
1107
|
+
# 尝试第一个常见的 Python 文件名(带防御性检查,如果不存在会跳过)
|
|
1108
|
+
common_py_names = ['app.py', 'main.py', 'run.py', 'server.py']
|
|
1109
|
+
py_name = common_py_names[0] # 使用第一个作为默认
|
|
1110
|
+
if is_archive and extracted_dir_name:
|
|
1111
|
+
test_path = f"{extracted_dir_name}/{py_name}"
|
|
1112
|
+
else:
|
|
1113
|
+
test_path = py_name
|
|
1114
|
+
# 命令已包含防御性检查,如果文件不存在会失败
|
|
1115
|
+
start_cmd = f"[ -f {test_path} ] && nohup python {test_path} > /root/app.log 2>&1 &"
|
|
1116
|
+
|
|
1117
|
+
elif deploy_language == 'nodejs':
|
|
1118
|
+
# Node.js 应用:查找 package.json
|
|
1119
|
+
if executable_files['package_json']:
|
|
1120
|
+
package_json_path = executable_files['package_json']
|
|
1121
|
+
if is_archive and extracted_dir_name:
|
|
1122
|
+
package_json_dir = extracted_dir_name
|
|
1123
|
+
else:
|
|
1124
|
+
package_json_dir = "."
|
|
1125
|
+
|
|
1126
|
+
# 检查是否有 start.sh
|
|
1127
|
+
shell_scripts = executable_files['shell_scripts']
|
|
1128
|
+
start_script = None
|
|
1129
|
+
for script in shell_scripts:
|
|
1130
|
+
if 'start' in script.lower():
|
|
1131
|
+
start_script = script.split('/')[-1]
|
|
1132
|
+
break
|
|
1133
|
+
|
|
1134
|
+
if start_script:
|
|
1135
|
+
# 即使有 start.sh,也先执行 npm install 确保依赖已安装
|
|
1136
|
+
script_path_for_cmd = start_script # 使用相对路径(文件名),因为 work_dir 会处理 cd 或已在当前目录
|
|
1137
|
+
start_cmd = f"command -v npm >/dev/null 2>&1 && [ -f package.json ] && npm install && [ -f {script_path_for_cmd} ] && chmod +x {script_path_for_cmd} && nohup ./{script_path_for_cmd} > /root/app.log 2>&1 &"
|
|
1138
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Node.js start command with script: {start_script}")
|
|
1139
|
+
else:
|
|
1140
|
+
# 使用 npm start,必须先执行 npm install
|
|
1141
|
+
start_cmd = f"command -v npm >/dev/null 2>&1 && [ -f package.json ] && npm install && nohup npm start > /root/app.log 2>&1 &"
|
|
1142
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Node.js start command with npm start")
|
|
1143
|
+
else:
|
|
1144
|
+
# 尝试查找 js 文件
|
|
1145
|
+
js_files = executable_files['js_files']
|
|
1146
|
+
if js_files:
|
|
1147
|
+
# 优先查找 index.js, app.js, server.js
|
|
1148
|
+
preferred_names = ['index.js', 'app.js', 'server.js', 'main.js']
|
|
1149
|
+
js_file = None
|
|
1150
|
+
for preferred in preferred_names:
|
|
1151
|
+
for js_path in js_files:
|
|
1152
|
+
if js_path.endswith(preferred):
|
|
1153
|
+
js_file = js_path.split('/')[-1]
|
|
1154
|
+
break
|
|
1155
|
+
if js_file:
|
|
1156
|
+
break
|
|
1157
|
+
|
|
1158
|
+
if not js_file and js_files:
|
|
1159
|
+
js_file = js_files[0].split('/')[-1]
|
|
1160
|
+
|
|
1161
|
+
if js_file:
|
|
1162
|
+
if is_archive and extracted_dir_name:
|
|
1163
|
+
js_file = f"{extracted_dir_name}/{js_file}"
|
|
1164
|
+
start_cmd = f"[ -f {js_file} ] && nohup node {js_file} > /root/app.log 2>&1 &"
|
|
1165
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Node.js start command for {js_file}")
|
|
1166
|
+
else:
|
|
1167
|
+
return None
|
|
1168
|
+
else:
|
|
1169
|
+
return None
|
|
1170
|
+
|
|
1171
|
+
elif deploy_language == 'golang':
|
|
1172
|
+
# Go 应用:查找二进制文件
|
|
1173
|
+
go_binaries = executable_files['go_binaries']
|
|
1174
|
+
# Go 二进制文件通常没有扩展名,且名称可能包含路径
|
|
1175
|
+
# 尝试查找常见的二进制文件名
|
|
1176
|
+
common_bin_names = ['app', 'main', 'server', 'application']
|
|
1177
|
+
binary_file = None
|
|
1178
|
+
|
|
1179
|
+
for bin_name in common_bin_names:
|
|
1180
|
+
for file_path in files_in_archive:
|
|
1181
|
+
file_name_only = file_path.split('/')[-1]
|
|
1182
|
+
if file_name_only == bin_name or file_name_only.startswith(bin_name):
|
|
1183
|
+
binary_file = file_path.split('/')[-1]
|
|
1184
|
+
break
|
|
1185
|
+
if binary_file:
|
|
1186
|
+
break
|
|
1187
|
+
|
|
1188
|
+
if binary_file:
|
|
1189
|
+
if is_archive and extracted_dir_name:
|
|
1190
|
+
binary_file = f"{extracted_dir_name}/{binary_file}"
|
|
1191
|
+
start_cmd = f"[ -f {binary_file} ] && chmod +x {binary_file} && nohup ./{binary_file} > /root/app.log 2>&1 &"
|
|
1192
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Golang start command for {binary_file}")
|
|
1193
|
+
else:
|
|
1194
|
+
# 尝试直接使用文件名(去掉扩展名)
|
|
1195
|
+
base_name = file_name.rsplit('.', 1)[0] if '.' in file_name else file_name
|
|
1196
|
+
if is_archive and extracted_dir_name:
|
|
1197
|
+
test_path = f"{extracted_dir_name}/{base_name}"
|
|
1198
|
+
else:
|
|
1199
|
+
test_path = base_name
|
|
1200
|
+
start_cmd = f"[ -f {test_path} ] && chmod +x {test_path} && nohup ./{test_path} > /root/app.log 2>&1 & || true"
|
|
1201
|
+
if not start_cmd:
|
|
1202
|
+
return None
|
|
1203
|
+
|
|
1204
|
+
elif deploy_language == 'docker':
|
|
1205
|
+
# Docker 应用:检查是否有 Dockerfile,然后生成 docker build 和 docker run 命令
|
|
1206
|
+
dockerfile_path = executable_files.get('dockerfile')
|
|
1207
|
+
|
|
1208
|
+
if not dockerfile_path:
|
|
1209
|
+
return None
|
|
1210
|
+
|
|
1211
|
+
# 生成 Docker 命令
|
|
1212
|
+
# 使用应用名称作为镜像和容器名称的基础(如果没有,使用默认值)
|
|
1213
|
+
image_name = (application_name or 'app').lower().replace(' ', '-').replace('_', '-')
|
|
1214
|
+
container_name = image_name
|
|
1215
|
+
|
|
1216
|
+
# 确定 Dockerfile 的路径
|
|
1217
|
+
if is_archive and extracted_dir_name:
|
|
1218
|
+
# 如果 Dockerfile 在解压目录中
|
|
1219
|
+
dockerfile_dir = extracted_dir_name
|
|
1220
|
+
dockerfile_for_build = "Dockerfile" # 在解压目录中,使用相对路径
|
|
1221
|
+
elif is_archive:
|
|
1222
|
+
# 解压到当前目录
|
|
1223
|
+
dockerfile_dir = "."
|
|
1224
|
+
dockerfile_for_build = "Dockerfile"
|
|
1225
|
+
else:
|
|
1226
|
+
# 不是压缩包,使用文件所在目录
|
|
1227
|
+
dockerfile_dir = "."
|
|
1228
|
+
dockerfile_for_build = "Dockerfile"
|
|
1229
|
+
|
|
1230
|
+
# 构建 Docker 命令
|
|
1231
|
+
port_mapping = f"-p {port}:{port} " if port else ""
|
|
1232
|
+
|
|
1233
|
+
# 检查 Docker 是否安装
|
|
1234
|
+
docker_check = "command -v docker >/dev/null 2>&1 && "
|
|
1235
|
+
|
|
1236
|
+
# 停止并删除旧容器(如果存在)
|
|
1237
|
+
stop_old_container = f"docker stop {container_name} 2>/dev/null || true && docker rm {container_name} 2>/dev/null || true && "
|
|
1238
|
+
|
|
1239
|
+
# 构建镜像(检查 Dockerfile 是否存在)
|
|
1240
|
+
build_cmd = f"[ -f {dockerfile_for_build} ] && docker build -t {image_name}:latest . && "
|
|
1241
|
+
|
|
1242
|
+
# 运行容器
|
|
1243
|
+
run_cmd = f"docker run -d --name {container_name} {port_mapping}{image_name}:latest"
|
|
1244
|
+
|
|
1245
|
+
# 组合命令
|
|
1246
|
+
start_cmd = f"{docker_check}{stop_old_container}{build_cmd}{run_cmd}"
|
|
1247
|
+
logger.info(f"[_generate_start_command_by_rules] Generated Docker start command for {container_name}")
|
|
1248
|
+
|
|
1249
|
+
else:
|
|
1250
|
+
# 未知语言类型,返回 None
|
|
1251
|
+
return None
|
|
1252
|
+
|
|
1253
|
+
# 组合命令
|
|
1254
|
+
if is_archive:
|
|
1255
|
+
if work_dir:
|
|
1256
|
+
final_cmd = f"{extract_cmd}{work_dir} && {start_cmd}"
|
|
1257
|
+
else:
|
|
1258
|
+
final_cmd = f"{extract_cmd} && {start_cmd}"
|
|
1259
|
+
else:
|
|
1260
|
+
final_cmd = start_cmd
|
|
1261
|
+
|
|
1262
|
+
return final_cmd
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _generate_stop_command_by_rules(
|
|
1266
|
+
deploy_language: str,
|
|
1267
|
+
file_analysis: Dict,
|
|
1268
|
+
extracted_dir_name: Optional[str] = None,
|
|
1269
|
+
application_name: Optional[str] = None
|
|
1270
|
+
) -> str:
|
|
1271
|
+
"""
|
|
1272
|
+
根据规则生成停止命令
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
生成的停止命令(总是返回有效命令,未知语言使用通用命令)
|
|
1276
|
+
"""
|
|
1277
|
+
# 语言到停止命令的映射
|
|
1278
|
+
stop_commands = {
|
|
1279
|
+
'java': "pkill -f 'java -jar' || true",
|
|
1280
|
+
'python': "pkill -f 'python.*\\.py' || true",
|
|
1281
|
+
'nodejs': "pkill -f 'node.*\\.js' || pkill -f 'npm start' || true",
|
|
1282
|
+
'golang': "pkill -f './app' || pkill -f './main' || pkill -f './server' || true",
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
# Docker 需要特殊处理(使用容器名)
|
|
1286
|
+
if deploy_language == 'docker':
|
|
1287
|
+
container_name = (application_name or 'app').lower().replace(' ', '-').replace('_', '-')
|
|
1288
|
+
stop_cmd = f"docker stop {container_name} 2>/dev/null || true && docker rm {container_name} 2>/dev/null || true"
|
|
1289
|
+
else:
|
|
1290
|
+
# 使用映射表,未知语言使用通用命令
|
|
1291
|
+
stop_cmd = stop_commands.get(deploy_language, "pkill -f 'app' || true")
|
|
1292
|
+
|
|
1293
|
+
return stop_cmd
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def _generate_start_stop_commands_by_rules(
|
|
1297
|
+
file_path: str,
|
|
1298
|
+
deploy_language: str,
|
|
1299
|
+
application_name: Optional[str] = None,
|
|
1300
|
+
port: Optional[int] = None
|
|
1301
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
1302
|
+
"""
|
|
1303
|
+
根据工程规则生成启动和停止命令
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
file_path: 部署文件路径
|
|
1307
|
+
deploy_language: 部署语言类型
|
|
1308
|
+
application_name: 应用名称(用于 Docker 容器命名等)
|
|
1309
|
+
port: 应用端口(用于 Docker 端口映射等)
|
|
1310
|
+
|
|
1311
|
+
Returns:
|
|
1312
|
+
(start_command, stop_command): 如果无法通过规则生成则返回 (None, None)
|
|
1313
|
+
"""
|
|
1314
|
+
try:
|
|
1315
|
+
# 分析部署文件
|
|
1316
|
+
file_analysis = _analyze_deployment_file(file_path)
|
|
1317
|
+
extracted_dir_name = file_analysis.get('extracted_dir_name')
|
|
1318
|
+
|
|
1319
|
+
# 生成启动命令
|
|
1320
|
+
start_cmd = _generate_start_command_by_rules(
|
|
1321
|
+
file_path, deploy_language, file_analysis, extracted_dir_name, application_name, port
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
# 生成停止命令
|
|
1325
|
+
stop_cmd = _generate_stop_command_by_rules(
|
|
1326
|
+
deploy_language, file_analysis, extracted_dir_name, application_name
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
# 记录结果
|
|
1330
|
+
if start_cmd:
|
|
1331
|
+
logger.info(f"[_generate_start_stop_commands_by_rules] Successfully generated start command for {deploy_language}")
|
|
1332
|
+
else:
|
|
1333
|
+
logger.warning(f"[_generate_start_stop_commands_by_rules] Failed to generate start command for {deploy_language}")
|
|
1334
|
+
|
|
1335
|
+
logger.info(f"[_generate_start_stop_commands_by_rules] Generated stop command for {deploy_language}")
|
|
1336
|
+
|
|
1337
|
+
return (start_cmd, stop_cmd)
|
|
1338
|
+
|
|
1339
|
+
except Exception as e:
|
|
1340
|
+
logger.warning(f"[_generate_start_stop_commands_by_rules] Failed to generate commands: {e}")
|
|
1341
|
+
return (None, None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alibaba-cloud-ops-mcp-server
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.27
|
|
4
4
|
Summary: A MCP server for Alibaba Cloud
|
|
5
5
|
Author-email: Zheng Dayu <dayu.zdy@alibaba-inc.com>, Zhao Shuaibo <zhaoshuaibo.zsb@alibaba-inc.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,7 +9,10 @@ Requires-Dist: alibabacloud-cms20190101>=3.1.4
|
|
|
9
9
|
Requires-Dist: alibabacloud-credentials>=1.0.3
|
|
10
10
|
Requires-Dist: alibabacloud-ecs20140526>=6.1.0
|
|
11
11
|
Requires-Dist: alibabacloud-oos20190601>=3.5.0
|
|
12
|
+
Requires-Dist: alibabacloud-openapi-util>=0.2.0
|
|
12
13
|
Requires-Dist: alibabacloud-oss-v2>=1.2.0
|
|
14
|
+
Requires-Dist: alibabacloud-tea-openapi>=0.3.0
|
|
15
|
+
Requires-Dist: alibabacloud-tea-util>=0.3.0
|
|
13
16
|
Requires-Dist: click>=8.1.8
|
|
14
17
|
Requires-Dist: fastmcp==2.8.0
|
|
15
18
|
Requires-Dist: pydantic==2.11.3
|
|
@@ -23,6 +26,17 @@ Description-Content-Type: text/markdown
|
|
|
23
26
|
|
|
24
27
|
Alibaba Cloud Ops MCP Server is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that provides seamless integration with Alibaba Cloud APIs, enabling AI assistants to operate resources on Alibaba Cloud, supporting ECS, Cloud Monitor, OOS, OSS, VPC, RDS and other widely used cloud products. It also enables AI assistants to analyze, build, and deploy applications to Alibaba Cloud ECS instances.
|
|
25
28
|
|
|
29
|
+
## MCP Maketplace Integration
|
|
30
|
+
|
|
31
|
+
* [Qoder](https://qoder.com) <a href="qoder://aicoding.aicoding-deeplink/mcp/add?name=alibaba-cloud-ops-mcp-server&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMiUyQyUyMmFyZ3MlMjIlM0ElNUIlMjJhbGliYWJhLWNsb3VkLW9wcy1tY3Atc2VydmVyJTQwbGF0ZXN0JTIyJTVEJTJDJTIyZW52JTIyJTNBJTdCJTIyQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEJTIyJTNBJTIyWW91ciUyMEFjY2VzcyUyMEtleSUyMElkJTIyJTJDJTIyQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCUyMiUzQSUyMllvdXIlMjBBY2Nlc3MlMjBLZXklMjBTRUNSRVQlMjIlN0QlN0Q%3D"><img src="./image/qoder.svg" alt="Install MCP Server" height="20"></a>
|
|
32
|
+
* [Cursor](https://docs.cursor.com/tools) [](https://cursor.com/en/install-mcp?name=alibaba-cloud-ops-mcp-server&config=eyJ0aW1lb3V0Ijo2MDAsImNvbW1hbmQiOiJ1dnggYWxpYmFiYS1jbG91ZC1vcHMtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEIjoiWW91ciBBY2Nlc3MgS2V5IElkIiwiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCI6IllvdXIgQWNjZXNzIEtleSBTZWNyZXQifX0%3D)
|
|
33
|
+
* [Cline](https://cline.bot/mcp-marketplace)
|
|
34
|
+
* [ModelScope](https://www.modelscope.cn/mcp/servers/@aliyun/alibaba-cloud-ops-mcp-server?lang=en_US)
|
|
35
|
+
* [Lingma](https://lingma.aliyun.com/)
|
|
36
|
+
* [Smithery AI](https://smithery.ai/server/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
37
|
+
* [FC-Function AI](https://cap.console.aliyun.com/template-detail?template=237)
|
|
38
|
+
* [Alibaba Cloud Model Studio](https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/alibaba-cloud-ops)
|
|
39
|
+
|
|
26
40
|
## Features
|
|
27
41
|
|
|
28
42
|
- **ECS Management**: Create, start, stop, reboot, delete instances, run commands, view instances, regions, zones, images, security groups, and more
|
|
@@ -70,17 +84,6 @@ To use `alibaba-cloud-ops-mcp-server` MCP Server with any other MCP Client, you
|
|
|
70
84
|
|
|
71
85
|
[For detailed parameter description, see MCP startup parameter document](./README_mcp_args.md)
|
|
72
86
|
|
|
73
|
-
## MCP Maketplace Integration
|
|
74
|
-
|
|
75
|
-
* [Qoder](https://qoder.com) <a href="qoder://aicoding.aicoding-deeplink/mcp/add?name=alibaba-cloud-ops-mcp-server&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMiUyQyUyMmFyZ3MlMjIlM0ElNUIlMjJhbGliYWJhLWNsb3VkLW9wcy1tY3Atc2VydmVyJTQwbGF0ZXN0JTIyJTVEJTJDJTIyZW52JTIyJTNBJTdCJTIyQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEJTIyJTNBJTIyWW91ciUyMEFjY2VzcyUyMEtleSUyMElkJTIyJTJDJTIyQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCUyMiUzQSUyMllvdXIlMjBBY2Nlc3MlMjBLZXklMjBTRUNSRVQlMjIlN0QlN0Q%3D"><img src="./image/qoder.svg" alt="Install MCP Server" height="20"></a>
|
|
76
|
-
* [Cursor](https://docs.cursor.com/tools) [](https://cursor.com/en/install-mcp?name=alibaba-cloud-ops-mcp-server&config=eyJ0aW1lb3V0Ijo2MDAsImNvbW1hbmQiOiJ1dnggYWxpYmFiYS1jbG91ZC1vcHMtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX0lEIjoiWW91ciBBY2Nlc3MgS2V5IElkIiwiQUxJQkFCQV9DTE9VRF9BQ0NFU1NfS0VZX1NFQ1JFVCI6IllvdXIgQWNjZXNzIEtleSBTZWNyZXQifX0%3D)
|
|
77
|
-
* [Cline](https://cline.bot/mcp-marketplace)
|
|
78
|
-
* [ModelScope](https://www.modelscope.cn/mcp/servers/@aliyun/alibaba-cloud-ops-mcp-server?lang=en_US)
|
|
79
|
-
* [Lingma](https://lingma.aliyun.com/)
|
|
80
|
-
* [Smithery AI](https://smithery.ai/server/@aliyun/alibaba-cloud-ops-mcp-server)
|
|
81
|
-
* [FC-Function AI](https://cap.console.aliyun.com/template-detail?template=237)
|
|
82
|
-
* [Alibaba Cloud Model Studio](https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/alibaba-cloud-ops)
|
|
83
|
-
|
|
84
87
|
## Know More
|
|
85
88
|
|
|
86
89
|
* [Alibaba Cloud Ops MCP Server is ready to use out of the box!!](https://developer.aliyun.com/article/1661348)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
alibaba_cloud_ops_mcp_server/__init__.py,sha256=BaluUNyRz8Qw-X7Y0ywDezwbkqiSvWlSYn2452XeGcA,213
|
|
2
2
|
alibaba_cloud_ops_mcp_server/__main__.py,sha256=Q40p2HtWGvxj1JLvS7dn95NLzDhJNQ6JAgLLyCb4Y50,63
|
|
3
3
|
alibaba_cloud_ops_mcp_server/config.py,sha256=PizctjXsQUWoMWBY1dFjNffVlZr9K6hNvqA4DpayR_o,513
|
|
4
|
-
alibaba_cloud_ops_mcp_server/server.py,sha256=
|
|
4
|
+
alibaba_cloud_ops_mcp_server/server.py,sha256=mt3LtII8CObvUwsC62ZlJjT0LpsGxz_fM2ZFg6GSuOk,16159
|
|
5
5
|
alibaba_cloud_ops_mcp_server/settings.py,sha256=R1jvMtgErWn_1MZ2Gq3xzBZeMtkGpSHvVutInh0Ix4s,167
|
|
6
6
|
alibaba_cloud_ops_mcp_server/alibabacloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
alibaba_cloud_ops_mcp_server/alibabacloud/api_meta_client.py,sha256=t2TSc0Gzcy_uEcaCgiHHuLoMiEGu3-NCtYmwYjyPWsY,7973
|
|
@@ -11,14 +11,14 @@ alibaba_cloud_ops_mcp_server/alibabacloud/static/PROMPT_UNDERSTANDING.md,sha256=
|
|
|
11
11
|
alibaba_cloud_ops_mcp_server/alibabacloud/static/__init__.py,sha256=wJrYamaIb7e_kA4ILZpdP1f1TUUTXMGqEhA4IbSZ2Ts,230
|
|
12
12
|
alibaba_cloud_ops_mcp_server/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
alibaba_cloud_ops_mcp_server/tools/api_tools.py,sha256=n7FLDAlnTSIk38iQqT1ABwAskIyswO_tBGTm34SPMpg,10632
|
|
14
|
-
alibaba_cloud_ops_mcp_server/tools/application_management_tools.py,sha256=
|
|
14
|
+
alibaba_cloud_ops_mcp_server/tools/application_management_tools.py,sha256=_cg1APuCZOzcayCRFlCeqeEBdWR8-6mrLtHMrVnGWP0,60274
|
|
15
15
|
alibaba_cloud_ops_mcp_server/tools/cms_tools.py,sha256=BmPTiP8wu9DsEHBQsvR7JH9nFkcKMTBuNuafFqSfVxU,4308
|
|
16
16
|
alibaba_cloud_ops_mcp_server/tools/common_api_tools.py,sha256=ccQAWqS1I9F-fdOdjLcXN-dIhNqSbZV8T5ODuGXlfXM,2711
|
|
17
17
|
alibaba_cloud_ops_mcp_server/tools/local_tools.py,sha256=Sl7Vl2mVQMLyMIZQDLoE-xVF0vNz_haMEspyYPDe6Sg,13133
|
|
18
18
|
alibaba_cloud_ops_mcp_server/tools/oos_tools.py,sha256=cPEl05Y0rlNvCeO2SF46Y7Ewky4LXZ8__sD0_JCi8ek,13569
|
|
19
19
|
alibaba_cloud_ops_mcp_server/tools/oss_tools.py,sha256=6yKo1FqQN3n9I-eDUW8MrnIZTHthy-worKc8XIsn-Nw,7427
|
|
20
|
-
alibaba_cloud_ops_mcp_server-0.9.
|
|
21
|
-
alibaba_cloud_ops_mcp_server-0.9.
|
|
22
|
-
alibaba_cloud_ops_mcp_server-0.9.
|
|
23
|
-
alibaba_cloud_ops_mcp_server-0.9.
|
|
24
|
-
alibaba_cloud_ops_mcp_server-0.9.
|
|
20
|
+
alibaba_cloud_ops_mcp_server-0.9.27.dist-info/METADATA,sha256=GitsYr48ksn759SjlgTlt-20JDikUf1tAnF0aKM2mRA,8933
|
|
21
|
+
alibaba_cloud_ops_mcp_server-0.9.27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
+
alibaba_cloud_ops_mcp_server-0.9.27.dist-info/entry_points.txt,sha256=ESGAWXKEp184forhs7VzTD4P1AUdZz6vCW6hRUKITGw,83
|
|
23
|
+
alibaba_cloud_ops_mcp_server-0.9.27.dist-info/licenses/LICENSE,sha256=gQgVkp2ttRCjodiPpXZZR-d7JnrYIYNiHk1YDUYgpa4,11331
|
|
24
|
+
alibaba_cloud_ops_mcp_server-0.9.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|