Merge 6d1a26a3151627816e2fc67db3adb5d177b5f8ca into d78e13f545136fcbba1feceecc5e0485a06c33a6
This commit is contained in:
commit
111fe6628b
@ -160,7 +160,7 @@ We are currently prioritizing:
|
||||
- **Skills**: For skill contributions, head to [ClawHub](https://clawhub.ai/) — the community hub for OpenClaw skills.
|
||||
- **Performance**: Optimizing token usage and compaction logic.
|
||||
|
||||
Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels!
|
||||
Check the [good first issues](https://github.com/openclaw/openclaw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to get started!
|
||||
|
||||
## Maintainers
|
||||
|
||||
|
||||
38
PR_DESCRIPTION.md
Normal file
38
PR_DESCRIPTION.md
Normal file
@ -0,0 +1,38 @@
|
||||
# PR Description
|
||||
|
||||
## What & Why
|
||||
|
||||
Improved the "good first issue" link in CONTRIBUTING.md to directly filter issues with the label, making it easier for new contributors to find entry points.
|
||||
|
||||
### Before
|
||||
```
|
||||
Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels!
|
||||
```
|
||||
- Required manual filtering
|
||||
- Extra step for newcomers
|
||||
|
||||
### After
|
||||
```
|
||||
Check the [good first issues](https://github.com/openclaw/openclaw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to get started!
|
||||
```
|
||||
- One-click access to filtered issues
|
||||
- Better newcomer experience
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [x] Documentation improvement
|
||||
|
||||
## Testing
|
||||
- [x] Verified the link works correctly
|
||||
- [x] Checked markdown rendering
|
||||
|
||||
## Checklist
|
||||
- [x] Change is focused (single concern)
|
||||
- [x] Description explains what & why
|
||||
- [ ] ~~Screenshots included~~ (N/A - text change only)
|
||||
|
||||
---
|
||||
|
||||
**Note:** This is my first contribution to OpenClaw. Excited to be part of the community! 🦞
|
||||
133
skills/aliyun-ecs-skill/README.md
Normal file
133
skills/aliyun-ecs-skill/README.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Aliyun ECS Skill for OpenClaw
|
||||
|
||||
阿里云ECS(弹性计算服务)管理技能,让你通过OpenClaw用自然语言管理阿里云服务器。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ **实例管理** - 查询、启动、停止、重启ECS实例
|
||||
- ✅ **监控查询** - 查看CPU、内存、网络等监控指标
|
||||
- ✅ **快照管理** - 创建、查看、回滚磁盘快照
|
||||
- ✅ **安全组** - 管理防火墙规则
|
||||
- 🔄 **远程命令** - 通过云助手在实例上执行命令(开发中)
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 进入skill目录
|
||||
cd ~/.openclaw/skills/aliyun-ecs-skill
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 配置阿里云密钥
|
||||
./scripts/setup.sh --access-key-id YOUR_ACCESS_KEY_ID --access-key-secret YOUR_ACCESS_KEY_SECRET
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 查询地域列表
|
||||
```bash
|
||||
aliyun-ecs regions
|
||||
```
|
||||
|
||||
### 查询实例列表
|
||||
```bash
|
||||
aliyun-ecs list --region cn-hangzhou
|
||||
```
|
||||
|
||||
### 启动/停止/重启实例
|
||||
```bash
|
||||
aliyun-ecs start --region cn-hangzhou --id i-bp67acfmxazb4p****
|
||||
aliyun-ecs stop --region cn-hangzhou --id i-bp67acfmxazb4p****
|
||||
aliyun-ecs restart --region cn-hangzhou --id i-bp67acfmxazb4p****
|
||||
```
|
||||
|
||||
### 创建快照
|
||||
```bash
|
||||
aliyun-ecs snapshot create --region cn-hangzhou --disk-id d-bp67acfmxazb4p**** --name "backup-20260312"
|
||||
```
|
||||
|
||||
### 管理安全组
|
||||
```bash
|
||||
# 查看安全组列表
|
||||
aliyun-ecs security-group list --region cn-hangzhou
|
||||
|
||||
# 添加规则(开放80端口)
|
||||
aliyun-ecs security-group add --region cn-hangzhou --group-id sg-bp67acfmxazb4p**** --port 80
|
||||
|
||||
# 删除规则
|
||||
aliyun-ecs security-group remove --region cn-hangzhou --group-id sg-bp67acfmxazb4p**** --port 80
|
||||
```
|
||||
|
||||
## 与腾讯云Lighthouse的区别
|
||||
|
||||
| 对比项 | 阿里云ECS | 腾讯云Lighthouse |
|
||||
|--------|-----------|------------------|
|
||||
| 定位 | 企业级弹性计算 | 轻量应用服务器 |
|
||||
| 目标用户 | 中大型企业、开发者 | 个人开发者、小企业 |
|
||||
| 计费方式 | 包年包月/按量付费 | 套餐包(更便宜) |
|
||||
| 功能丰富度 | 更丰富(SLB、VPC等) | 简洁够用 |
|
||||
| 市场占有率 | ~35-40%(第一) | ~15-18%(第三) |
|
||||
|
||||
## 典型使用场景
|
||||
|
||||
### 场景1: 快速部署测试环境
|
||||
```
|
||||
"帮我创建一台杭州区域的2核4G服务器"
|
||||
→ 选择镜像 → 配置安全组 → 启动实例
|
||||
```
|
||||
|
||||
### 场景2: 大促前扩容
|
||||
```
|
||||
"双11快到了,给所有生产服务器做快照备份"
|
||||
→ 批量创建快照
|
||||
```
|
||||
|
||||
### 场景3: 安全加固
|
||||
```
|
||||
"检查所有服务器的安全组,只开放必要的端口"
|
||||
→ 列出规则 → 识别风险 → 清理多余规则
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
配置文件位于 `~/.aliyun/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"accessKeyId": "YOUR_ACCESS_KEY_ID",
|
||||
"accessKeySecret": "YOUR_ACCESS_KEY_SECRET",
|
||||
"defaultRegion": "cn-hangzhou",
|
||||
"endpoint": "ecs.aliyuncs.com"
|
||||
}
|
||||
```
|
||||
|
||||
**安全建议**: 使用RAM子账号,只授予ECS相关权限(`ecs:*`)
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] 基础实例管理
|
||||
- [x] 快照管理
|
||||
- [x] 安全组管理
|
||||
- [ ] 监控告警
|
||||
- [ ] 远程命令执行(云助手)
|
||||
- [ ] 自动伸缩
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交PR!请确保:
|
||||
1. 代码通过ESLint检查
|
||||
2. 添加必要的测试
|
||||
3. 更新文档
|
||||
|
||||
## 许可
|
||||
|
||||
MIT
|
||||
|
||||
## 作者
|
||||
|
||||
Leo & AI Agent
|
||||
|
||||
---
|
||||
|
||||
**关联**: 本项目与 [tencentcloud-lighthouse-skill](https://clawhub.ai/skills/tencentcloud-lighthouse-skill) 形成互补,共同覆盖中国主流云服务商。
|
||||
168
skills/aliyun-ecs-skill/SKILL.md
Normal file
168
skills/aliyun-ecs-skill/SKILL.md
Normal file
@ -0,0 +1,168 @@
|
||||
---
|
||||
name: aliyun-ecs-skill
|
||||
description: Manage Alibaba Cloud ECS (Elastic Compute Service) — query instances, monitoring, firewall, snapshots, remote execution. Use when user asks about ECS, 阿里云服务器, or Alibaba Cloud. NOT for Tencent Cloud or other cloud providers.
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "☁️",
|
||||
"requires": {},
|
||||
"install":
|
||||
[
|
||||
{
|
||||
"id": "node-aliyun-sdk",
|
||||
"kind": "node",
|
||||
"package": "@alicloud/openapi-client",
|
||||
"label": "Install AliCloud SDK",
|
||||
},
|
||||
{
|
||||
"id": "node-aliyun-ecs-sdk",
|
||||
"kind": "node",
|
||||
"package": "@alicloud/ecs20140526",
|
||||
"label": "Install AliCloud ECS SDK",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Aliyun ECS 云服务器运维
|
||||
|
||||
管理阿里云ECS(弹性计算服务)实例。
|
||||
|
||||
## 首次使用 — 自动设置
|
||||
|
||||
当用户首次要求管理阿里云服务器时,按以下流程操作:
|
||||
|
||||
### 步骤 1:检查当前状态
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/setup.sh --check-only
|
||||
```
|
||||
|
||||
如果输出显示一切 OK(SDK 已安装、config 已配置、ECS 已就绪),跳到「调用格式」。
|
||||
|
||||
### 步骤 2:如果未配置,引导用户提供密钥
|
||||
|
||||
告诉用户:
|
||||
> 我需要你的阿里云 API 密钥来连接 ECS 服务器。请提供:
|
||||
> 1. **AccessKey ID** — 阿里云 API 密钥 ID
|
||||
> 2. **AccessKey Secret** — 阿里云 API 密钥 Secret
|
||||
>
|
||||
> 你可以在 [阿里云控制台 > 访问控制 > AccessKey管理](https://ram.console.aliyun.com/manage/ak) 获取。
|
||||
>
|
||||
> ⚠️ 建议使用子账号,只授予ECS相关权限(ecs:*)
|
||||
|
||||
### 步骤 3:用户提供密钥后,运行自动设置
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/setup.sh --access-key-id "<用户提供的AccessKeyId>" --access-key-secret "<用户提供的AccessKeySecret>"
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
- 检查并安装阿里云SDK(如未安装)
|
||||
- 创建 `~/.aliyun/config.json` 配置文件
|
||||
- 写入 ECS 配置和密钥
|
||||
- 验证连接
|
||||
|
||||
设置完成后即可开始使用。
|
||||
|
||||
## 调用格式
|
||||
|
||||
所有命令使用以下格式:
|
||||
|
||||
```
|
||||
aliyun-ecs <command> [options]
|
||||
```
|
||||
|
||||
或直接使用 Node.js 脚本:
|
||||
|
||||
```bash
|
||||
node {baseDir}/src/index.js <command> [options]
|
||||
```
|
||||
|
||||
## 工具总览
|
||||
|
||||
| 类别 | 说明 |
|
||||
|------|------|
|
||||
| 地域查询 | 获取可用地域列表 |
|
||||
| 实例管理 | 查询、启动、停止、重启实例 |
|
||||
| 监控与告警 | 获取多指标监控数据 |
|
||||
| 快照管理 | 创建、查询、回滚快照 |
|
||||
| 安全组 | 规则增删改查 |
|
||||
|
||||
## 常用操作
|
||||
|
||||
### 获取地域列表
|
||||
|
||||
```bash
|
||||
aliyun-ecs regions
|
||||
```
|
||||
|
||||
### 实例管理
|
||||
|
||||
```bash
|
||||
# 查询实例列表
|
||||
aliyun-ecs list --region cn-hangzhou
|
||||
|
||||
# 查询指定实例
|
||||
aliyun-ecs info --region cn-hangzhou --id i-xxxxxxxxxx
|
||||
|
||||
# 启动实例
|
||||
aliyun-ecs start --region cn-hangzhou --id i-xxxxxxxxxx
|
||||
|
||||
# 停止实例
|
||||
aliyun-ecs stop --region cn-hangzhou --id i-xxxxxxxxxx
|
||||
|
||||
# 重启实例
|
||||
aliyun-ecs restart --region cn-hangzhou --id i-xxxxxxxxxx
|
||||
```
|
||||
|
||||
### 监控与告警
|
||||
|
||||
```bash
|
||||
# 获取监控数据(CPU、内存、网络)
|
||||
aliyun-ecs monitor --region cn-hangzhou --id i-xxxxxxxxxx --metrics CPU,Memory
|
||||
|
||||
# 支持的监控指标:
|
||||
# CPU - CPU使用率
|
||||
# Memory - 内存使用率
|
||||
# InternetIn - 公网入带宽(Kbps)
|
||||
# InternetOut - 公网出带宽(Kbps)
|
||||
# IntranetIn - 内网入带宽(Kbps)
|
||||
# IntranetOut - 内网出带宽(Kbps)
|
||||
```
|
||||
|
||||
### 快照管理
|
||||
|
||||
```bash
|
||||
# 创建快照
|
||||
aliyun-ecs snapshot create --region cn-hangzhou --disk-id d-xxxxxxxxxx --name "backup-20260312"
|
||||
|
||||
# 列出快照
|
||||
aliyun-ecs snapshot list --region cn-hangzhou --id i-xxxxxxxxxx
|
||||
|
||||
# 回滚快照
|
||||
aliyun-ecs snapshot rollback --region cn-hangzhou --disk-id d-xxxxxxxxxx --snapshot-id s-xxxxxxxxxx
|
||||
```
|
||||
|
||||
### 安全组(防火墙)
|
||||
|
||||
```bash
|
||||
# 查询安全组规则
|
||||
aliyun-ecs security-group list --region cn-hangzhou --group-id sg-xxxxxxxxxx
|
||||
|
||||
# 添加安全组规则(开放80端口)
|
||||
aliyun-ecs security-group add --region cn-hangzhou --group-id sg-xxxxxxxxxx --port 80 --protocol tcp
|
||||
|
||||
# 删除安全组规则
|
||||
aliyun-ecs security-group remove --region cn-hangzhou --group-id sg-xxxxxxxxxx --port 80 --protocol tcp
|
||||
```
|
||||
|
||||
## 使用规范
|
||||
|
||||
1. **Region 参数规则**: 除 `regions` 外,所有操作都**必须**传入 `--region` 参数
|
||||
2. **首次使用流程**: 先调用 `regions` 获取地域列表 → 再调用 `list` 获取实例列表 → 记住 InstanceId 和 Region 供后续使用
|
||||
3. **危险操作前先确认**: 安全组修改、实例停止/重启、快照回滚等,先向用户确认
|
||||
4. **错误处理**: 如果调用失败,先用 `setup.sh --check-only` 诊断问题
|
||||
5. **建议子账号**: 为了安全,建议用户使用子账号AccessKey,只授予ECS相关权限
|
||||
6
skills/aliyun-ecs-skill/_meta.json
Normal file
6
skills/aliyun-ecs-skill/_meta.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "leo-and-ai-agent",
|
||||
"slug": "aliyun-ecs-skill",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": null
|
||||
}
|
||||
25
skills/aliyun-ecs-skill/package.json
Normal file
25
skills/aliyun-ecs-skill/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "aliyun-ecs-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "Aliyun ECS management skill for OpenClaw",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"aliyun-ecs": "./src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"openclaw",
|
||||
"aliyun",
|
||||
"ecs",
|
||||
"cloud",
|
||||
"skill"
|
||||
],
|
||||
"author": "Leo & AI Agent",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alicloud/openapi-client": "^0.4.10",
|
||||
"@alicloud/ecs20140526": "^7.0.0"
|
||||
}
|
||||
}
|
||||
261
skills/aliyun-ecs-skill/scripts/setup.sh
Executable file
261
skills/aliyun-ecs-skill/scripts/setup.sh
Executable file
@ -0,0 +1,261 @@
|
||||
#!/bin/bash
|
||||
# setup.sh — 阿里云ECS Skill 自动设置脚本
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="$HOME/.aliyun"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
|
||||
# 颜色
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 检查Node.js
|
||||
node_check() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo -e "${RED}✗ Node.js 未安装${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
if [ "$NODE_VERSION" -lt 16 ]; then
|
||||
echo -e "${RED}✗ Node.js 版本过低: $(node --version),需要 >= 16${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Node.js $(node --version)${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查阿里云SDK
|
||||
sdk_check() {
|
||||
# 优先检查本地 node_modules(skill 运行时从这里加载)
|
||||
if [ -d "$SCRIPT_DIR/../node_modules/@alicloud/openapi-client" ] && [ -d "$SCRIPT_DIR/../node_modules/@alicloud/ecs20140526" ]; then
|
||||
echo -e "${GREEN}✓ 阿里云SDK已安装${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}⚠ 阿里云SDK未安装${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 安装阿里云SDK
|
||||
sdk_install() {
|
||||
echo "正在安装阿里云SDK..."
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
# 创建package.json如果不存在
|
||||
if [ ! -f package.json ]; then
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "aliyun-ecs-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "Aliyun ECS management skill for OpenClaw",
|
||||
"main": "src/index.js",
|
||||
"dependencies": {
|
||||
"@alicloud/openapi-client": "^0.4.10",
|
||||
"@alicloud/ecs20140526": "^7.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
npm install
|
||||
echo -e "${GREEN}✓ 阿里云SDK安装完成${NC}"
|
||||
}
|
||||
|
||||
# 检查配置文件
|
||||
config_check() {
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
if grep -q "accessKeyId" "$CONFIG_FILE" 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ 配置文件已存在${NC}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}⚠ 配置文件不存在或无效${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 创建配置文件
|
||||
config_create() {
|
||||
local access_key_id="$1"
|
||||
local access_key_secret="$2"
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
{
|
||||
"accessKeyId": "$access_key_id",
|
||||
"accessKeySecret": "$access_key_secret",
|
||||
"defaultRegion": "cn-hangzhou",
|
||||
"endpoint": "ecs.aliyuncs.com"
|
||||
}
|
||||
EOF
|
||||
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
echo -e "${GREEN}✓ 配置文件创建完成 ($CONFIG_FILE)${NC}"
|
||||
}
|
||||
|
||||
# 验证连接
|
||||
test_connection() {
|
||||
echo "正在验证阿里云连接..."
|
||||
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
# 使用实际的阿里云API调用来验证连接
|
||||
node -e "
|
||||
const { default: OpenApi, \$OpenApi } = require('@alicloud/openapi-client');
|
||||
const Ecs20140526 = require('@alicloud/ecs20140526');
|
||||
const fs = require('fs');
|
||||
|
||||
const configPath = '$CONFIG_FILE';
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const clientConfig = new \$OpenApi({
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret: config.accessKeySecret,
|
||||
});
|
||||
clientConfig.endpoint = 'ecs.aliyuncs.com';
|
||||
|
||||
const client = new Ecs20140526(clientConfig);
|
||||
const response = await client.describeRegions(new Ecs20140526.DescribeRegionsRequest({}));
|
||||
|
||||
if (response.body && response.body.regions && response.body.regions.region) {
|
||||
console.log('Connection test passed');
|
||||
console.log('Available regions:', response.body.regions.region.length);
|
||||
return true;
|
||||
}
|
||||
throw new Error('Invalid response');
|
||||
} catch (error) {
|
||||
console.error('Connection failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
" 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ 连接验证成功${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ 连接验证失败,请检查密钥和网络${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
cat << EOF
|
||||
阿里云ECS Skill 设置脚本
|
||||
|
||||
用法:
|
||||
$0 [选项]
|
||||
|
||||
选项:
|
||||
--check-only 仅检查环境,不修改
|
||||
--access-key-id ID 阿里云AccessKey ID
|
||||
--access-key-secret S 阿里云AccessKey Secret
|
||||
--help 显示此帮助
|
||||
|
||||
示例:
|
||||
$0 --check-only
|
||||
$0 --access-key-id LTAIxxxxx --access-key-secret xxxxx
|
||||
EOF
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
local check_only=false
|
||||
local access_key_id=""
|
||||
local access_key_secret=""
|
||||
|
||||
# 解析参数
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--check-only)
|
||||
check_only=true
|
||||
shift
|
||||
;;
|
||||
--access-key-id)
|
||||
access_key_id="$2"
|
||||
shift 2
|
||||
;;
|
||||
--access-key-secret)
|
||||
access_key_secret="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知选项: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== 阿里云ECS Skill 环境检查 ==="
|
||||
echo ""
|
||||
|
||||
# 检查Node.js
|
||||
local node_ok=false
|
||||
if node_check; then
|
||||
node_ok=true
|
||||
fi
|
||||
|
||||
# 检查SDK
|
||||
local sdk_ok=false
|
||||
if sdk_check; then
|
||||
sdk_ok=true
|
||||
elif [ "$check_only" = false ] && [ -n "$access_key_id" ]; then
|
||||
sdk_install
|
||||
sdk_ok=true
|
||||
fi
|
||||
|
||||
# 检查配置
|
||||
local config_ok=false
|
||||
if config_check; then
|
||||
config_ok=true
|
||||
elif [ "$check_only" = false ] && [ -n "$access_key_id" ] && [ -n "$access_key_secret" ]; then
|
||||
config_create "$access_key_id" "$access_key_secret"
|
||||
config_ok=true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 检查汇总 ==="
|
||||
|
||||
if [ "$node_ok" = true ] && [ "$sdk_ok" = true ] && [ "$config_ok" = true ]; then
|
||||
echo -e "${GREEN}✓ 所有检查通过,环境已就绪${NC}"
|
||||
|
||||
if [ "$check_only" = false ]; then
|
||||
test_connection
|
||||
fi
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 环境未完全就绪${NC}"
|
||||
|
||||
if [ "$check_only" = true ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$access_key_id" ] || [ -z "$access_key_secret" ]; then
|
||||
echo ""
|
||||
echo "请提供阿里云AccessKey以完成设置:"
|
||||
echo " $0 --access-key-id YOUR_ID --access-key-secret YOUR_SECRET"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
335
skills/aliyun-ecs-skill/src/api/ecs.js
Normal file
335
skills/aliyun-ecs-skill/src/api/ecs.js
Normal file
@ -0,0 +1,335 @@
|
||||
const { default: OpenApi, $OpenApi } = require('@alicloud/openapi-client');
|
||||
const Ecs20140526 = require('@alicloud/ecs20140526');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 加载配置
|
||||
function loadConfig() {
|
||||
const configPath = path.join(process.env.HOME, '.aliyun', 'config.json');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error('配置文件不存在,请先运行 setup.sh 进行配置');
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
}
|
||||
|
||||
// 创建ECS客户端
|
||||
function createClient(regionId) {
|
||||
const config = loadConfig();
|
||||
|
||||
const clientConfig = new $OpenApi({
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret: config.accessKeySecret,
|
||||
});
|
||||
|
||||
clientConfig.endpoint = `ecs.${regionId}.aliyuncs.com`;
|
||||
|
||||
return new Ecs20140526(clientConfig);
|
||||
}
|
||||
|
||||
// 查询地域列表
|
||||
async function describeRegions() {
|
||||
const config = loadConfig();
|
||||
const clientConfig = new $OpenApi({
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret: config.accessKeySecret,
|
||||
});
|
||||
clientConfig.endpoint = 'ecs.aliyuncs.com';
|
||||
|
||||
const client = new Ecs20140526(clientConfig);
|
||||
const response = await client.describeRegions(new Ecs20140526.DescribeRegionsRequest({}));
|
||||
|
||||
return response.body.regions.region.map(r => ({
|
||||
regionId: r.regionId,
|
||||
regionEndpoint: r.regionEndpoint,
|
||||
localName: r.localName
|
||||
}));
|
||||
}
|
||||
|
||||
// 查询实例列表
|
||||
async function describeInstances(regionId, options = {}) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.DescribeInstancesRequest({
|
||||
regionId: regionId,
|
||||
pageSize: options.pageSize || 20,
|
||||
pageNumber: options.pageNumber || 1,
|
||||
});
|
||||
|
||||
if (options.instanceIds) {
|
||||
request.instanceIds = options.instanceIds;
|
||||
}
|
||||
|
||||
const response = await client.describeInstances(request);
|
||||
|
||||
if (!response.body.instances || !response.body.instances.instance) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.body.instances.instance.map(inst => ({
|
||||
instanceId: inst.instanceId,
|
||||
instanceName: inst.instanceName,
|
||||
status: inst.status,
|
||||
regionId: inst.regionId,
|
||||
zoneId: inst.zoneId,
|
||||
instanceType: inst.instanceType,
|
||||
cpu: inst.cpu,
|
||||
memory: inst.memory,
|
||||
osName: inst.osName,
|
||||
osType: inst.osType,
|
||||
publicIpAddress: inst.publicIpAddress?.ipAddress || [],
|
||||
privateIpAddress: inst.vpcAttributes?.privateIpAddress?.ipAddress || [],
|
||||
creationTime: inst.creationTime,
|
||||
expiredTime: inst.expiredTime,
|
||||
networkType: inst.networkType,
|
||||
internetChargeType: inst.internetChargeType,
|
||||
}));
|
||||
}
|
||||
|
||||
// 启动实例
|
||||
async function startInstance(regionId, instanceId) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.StartInstanceRequest({
|
||||
instanceId: instanceId,
|
||||
});
|
||||
|
||||
const response = await client.startInstance(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 停止实例
|
||||
async function stopInstance(regionId, instanceId, forceStop = false) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.StopInstanceRequest({
|
||||
instanceId: instanceId,
|
||||
forceStop: forceStop,
|
||||
});
|
||||
|
||||
const response = await client.stopInstance(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 重启实例
|
||||
async function rebootInstance(regionId, instanceId, forceStop = false) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.RebootInstanceRequest({
|
||||
instanceId: instanceId,
|
||||
forceStop: forceStop,
|
||||
});
|
||||
|
||||
const response = await client.rebootInstance(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取监控数据
|
||||
async function describeInstanceMonitorData(regionId, instanceId, period = 60, startTime, endTime) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.DescribeInstanceMonitorDataRequest({
|
||||
regionId: regionId,
|
||||
instanceId: instanceId,
|
||||
period: period,
|
||||
});
|
||||
|
||||
if (startTime) request.startTime = startTime;
|
||||
if (endTime) request.endTime = endTime;
|
||||
|
||||
const response = await client.describeInstanceMonitorData(request);
|
||||
|
||||
if (!response.body.monitorData || !response.body.monitorData.instanceMonitorData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.body.monitorData.instanceMonitorData.map(data => ({
|
||||
timestamp: data.timeStamp,
|
||||
cpu: data.CPU,
|
||||
memory: data.memory,
|
||||
internetIn: data.internetIn,
|
||||
internetOut: data.internetOut,
|
||||
intranetIn: data.intranetIn,
|
||||
intranetOut: data.intranetOut,
|
||||
}));
|
||||
}
|
||||
|
||||
// 创建快照
|
||||
async function createSnapshot(regionId, diskId, snapshotName, description = '') {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.CreateSnapshotRequest({
|
||||
diskId: diskId,
|
||||
snapshotName: snapshotName,
|
||||
description: description,
|
||||
});
|
||||
|
||||
const response = await client.createSnapshot(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
snapshotId: response.body.snapshotId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 查询快照
|
||||
async function describeSnapshots(regionId, instanceId, diskId) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.DescribeSnapshotsRequest({
|
||||
regionId: regionId,
|
||||
});
|
||||
|
||||
if (instanceId) request.instanceId = instanceId;
|
||||
if (diskId) request.diskIds = JSON.stringify([diskId]);
|
||||
|
||||
const response = await client.describeSnapshots(request);
|
||||
|
||||
if (!response.body.snapshots || !response.body.snapshots.snapshot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.body.snapshots.snapshot.map(snap => ({
|
||||
snapshotId: snap.snapshotId,
|
||||
snapshotName: snap.snapshotName,
|
||||
description: snap.description,
|
||||
status: snap.status,
|
||||
progress: snap.progress,
|
||||
creationTime: snap.creationTime,
|
||||
sourceDiskId: snap.sourceDiskId,
|
||||
sourceDiskType: snap.sourceDiskType,
|
||||
}));
|
||||
}
|
||||
|
||||
// 回滚快照
|
||||
async function resetDisk(regionId, diskId, snapshotId) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.ResetDiskRequest({
|
||||
diskId: diskId,
|
||||
snapshotId: snapshotId,
|
||||
});
|
||||
|
||||
const response = await client.resetDisk(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 查询安全组
|
||||
async function describeSecurityGroups(regionId, options = {}) {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.DescribeSecurityGroupsRequest({
|
||||
regionId: regionId,
|
||||
});
|
||||
|
||||
if (options.securityGroupId) {
|
||||
request.securityGroupIds = options.securityGroupId;
|
||||
}
|
||||
|
||||
const response = await client.describeSecurityGroups(request);
|
||||
|
||||
if (!response.body.securityGroups || !response.body.securityGroups.securityGroup) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.body.securityGroups.securityGroup.map(sg => ({
|
||||
securityGroupId: sg.securityGroupId,
|
||||
securityGroupName: sg.securityGroupName,
|
||||
description: sg.description,
|
||||
vpcId: sg.vpcId,
|
||||
creationTime: sg.creationTime,
|
||||
}));
|
||||
}
|
||||
|
||||
// 查询安全组规则
|
||||
async function describeSecurityGroupAttribute(regionId, securityGroupId, direction = 'ingress') {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.DescribeSecurityGroupAttributeRequest({
|
||||
regionId: regionId,
|
||||
securityGroupId: securityGroupId,
|
||||
direction: direction,
|
||||
});
|
||||
|
||||
const response = await client.describeSecurityGroupAttribute(request);
|
||||
|
||||
const rules = response.body.permissions?.permission;
|
||||
|
||||
if (!rules) return [];
|
||||
|
||||
return rules.map(rule => ({
|
||||
ipProtocol: rule.ipProtocol,
|
||||
portRange: rule.portRange,
|
||||
sourceCidrIp: rule.sourceCidrIp,
|
||||
destCidrIp: rule.destCidrIp,
|
||||
policy: rule.policy,
|
||||
description: rule.description,
|
||||
}));
|
||||
}
|
||||
|
||||
// 授权安全组规则
|
||||
async function authorizeSecurityGroup(regionId, securityGroupId, ipProtocol, portRange, sourceCidrIp = '0.0.0.0/0', description = '') {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.AuthorizeSecurityGroupRequest({
|
||||
regionId: regionId,
|
||||
securityGroupId: securityGroupId,
|
||||
ipProtocol: ipProtocol,
|
||||
portRange: portRange,
|
||||
sourceCidrIp: sourceCidrIp,
|
||||
description: description,
|
||||
policy: 'accept',
|
||||
});
|
||||
|
||||
const response = await client.authorizeSecurityGroup(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 撤销安全组规则
|
||||
async function revokeSecurityGroup(regionId, securityGroupId, ipProtocol, portRange, sourceCidrIp = '0.0.0.0/0') {
|
||||
const client = createClient(regionId);
|
||||
|
||||
const request = new Ecs20140526.RevokeSecurityGroupRequest({
|
||||
regionId: regionId,
|
||||
securityGroupId: securityGroupId,
|
||||
ipProtocol: ipProtocol,
|
||||
portRange: portRange,
|
||||
sourceCidrIp: sourceCidrIp,
|
||||
});
|
||||
|
||||
const response = await client.revokeSecurityGroup(request);
|
||||
return {
|
||||
requestId: response.body.requestId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
describeRegions,
|
||||
describeInstances,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
rebootInstance,
|
||||
describeInstanceMonitorData,
|
||||
createSnapshot,
|
||||
describeSnapshots,
|
||||
resetDisk,
|
||||
describeSecurityGroups,
|
||||
describeSecurityGroupAttribute,
|
||||
authorizeSecurityGroup,
|
||||
revokeSecurityGroup,
|
||||
};
|
||||
429
skills/aliyun-ecs-skill/src/index.js
Executable file
429
skills/aliyun-ecs-skill/src/index.js
Executable file
@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const ecs = require('./api/ecs');
|
||||
|
||||
// 格式化输出
|
||||
function formatTable(data, columns) {
|
||||
if (!data || data.length === 0) {
|
||||
return '暂无数据';
|
||||
}
|
||||
|
||||
// 获取每列最大宽度
|
||||
const widths = {};
|
||||
columns.forEach(col => {
|
||||
const headerLength = col.header.length;
|
||||
const maxDataLength = Math.max(...data.map(row => String(row[col.key] || '-').length));
|
||||
widths[col.key] = Math.max(headerLength, maxDataLength) + 2;
|
||||
});
|
||||
|
||||
// 生成表头
|
||||
let output = '|';
|
||||
columns.forEach(col => {
|
||||
output += ` ${col.header.padEnd(widths[col.key] - 1)}|`;
|
||||
});
|
||||
output += '\n';
|
||||
|
||||
// 生成分隔线
|
||||
output += '|';
|
||||
columns.forEach(col => {
|
||||
output += '-'.repeat(widths[col.key]) + '|';
|
||||
});
|
||||
output += '\n';
|
||||
|
||||
// 生成数据行
|
||||
data.forEach(row => {
|
||||
output += '|';
|
||||
columns.forEach(col => {
|
||||
const cellValue = row[col.key];
|
||||
const value = String(cellValue !== undefined && cellValue !== null ? cellValue : '-');
|
||||
output += ` ${value.padEnd(widths[col.key] - 1)}|`;
|
||||
});
|
||||
output += '\n';
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// 状态颜色映射
|
||||
function formatStatus(status) {
|
||||
const colors = {
|
||||
'Running': '\x1b[32m运行中\x1b[0m',
|
||||
'Stopped': '\x1b[31m已停止\x1b[0m',
|
||||
'Starting': '\x1b[33m启动中\x1b[0m',
|
||||
'Stopping': '\x1b[33m停止中\x1b[0m',
|
||||
};
|
||||
return colors[status] || status;
|
||||
}
|
||||
|
||||
// 显示帮助
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
阿里云ECS管理工具
|
||||
|
||||
用法:
|
||||
aliyun-ecs <command> [options]
|
||||
|
||||
命令:
|
||||
regions 查询所有可用地域
|
||||
list --region <region> 查询实例列表
|
||||
info --region <region> --id <id> 查询实例详情
|
||||
start --region <region> --id <id> 启动实例
|
||||
stop --region <region> --id <id> 停止实例
|
||||
restart --region <region> --id <id> 重启实例
|
||||
monitor --region <region> --id <id> [--metrics <metrics>] [--period <seconds>] [--minutes <n>] 查询监控数据
|
||||
snapshot list --region <region> --id <id> 查询快照列表
|
||||
snapshot create --region <region> --disk-id <id> --name <name> 创建快照
|
||||
snapshot rollback --region <region> --disk-id <id> --snapshot-id <id> 回滚快照
|
||||
security-group list --region <region> 查询安全组
|
||||
security-group rules --region <region> --group-id <id> 查询安全组规则
|
||||
security-group add --region <region> --group-id <id> --port <port> 添加安全组规则
|
||||
security-group remove --region <region> --group-id <id> --port <port> 删除安全组规则
|
||||
|
||||
示例:
|
||||
aliyun-ecs regions
|
||||
aliyun-ecs list --region cn-hangzhou
|
||||
aliyun-ecs info --region cn-hangzhou --id i-bp67acfmxazb4p****
|
||||
aliyun-ecs monitor --region cn-hangzhou --id i-bp67acfmxazb4p**** --metrics CPU,Memory
|
||||
aliyun-ecs monitor --region cn-hangzhou --id i-xxx --metrics CPU,Memory,InternetIn,InternetOut --period 60 --minutes 30
|
||||
`);
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = args[0];
|
||||
|
||||
// 解析选项
|
||||
const options = {};
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.replace(/^--/, '');
|
||||
// 检查下一个参数是否存在且不是选项
|
||||
const nextArg = args[i + 1];
|
||||
if (nextArg !== undefined && !nextArg.startsWith('--')) {
|
||||
options[key] = nextArg;
|
||||
i++; // 跳过已处理的值
|
||||
} else {
|
||||
options[key] = true;
|
||||
}
|
||||
} else if (command === 'snapshot' || command === 'security-group') {
|
||||
// 子命令处理
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'regions': {
|
||||
const regions = await ecs.describeRegions();
|
||||
console.log('\n可用地域列表:\n');
|
||||
console.log(formatTable(regions, [
|
||||
{ key: 'regionId', header: '地域ID' },
|
||||
{ key: 'localName', header: '名称' },
|
||||
{ key: 'regionEndpoint', header: 'Endpoint' },
|
||||
]));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
if (!options.region) {
|
||||
console.error('错误: 请提供 --region 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
const pageSize = parseInt(options['page-size']) || 20;
|
||||
const pageNumber = parseInt(options.page) || 1;
|
||||
const listOptions = {
|
||||
pageSize: pageSize,
|
||||
pageNumber: pageNumber,
|
||||
};
|
||||
const instances = await ecs.describeInstances(options.region, listOptions);
|
||||
console.log(`\n地域 ${options.region} 的实例列表 (第${pageNumber}页, 每页${pageSize}条):\n`);
|
||||
if (instances.length === 0) {
|
||||
if (pageNumber > 1) {
|
||||
console.log('该页无实例,可能是已到达末尾');
|
||||
} else {
|
||||
console.log('暂无实例');
|
||||
}
|
||||
} else {
|
||||
console.log(formatTable(instances.map(inst => ({
|
||||
...inst,
|
||||
status: formatStatus(inst.status),
|
||||
ip: inst.publicIpAddress.join(', ') || inst.privateIpAddress.join(', '),
|
||||
})), [
|
||||
{ key: 'instanceId', header: '实例ID' },
|
||||
{ key: 'instanceName', header: '名称' },
|
||||
{ key: 'status', header: '状态' },
|
||||
{ key: 'instanceType', header: '类型' },
|
||||
{ key: 'ip', header: 'IP地址' },
|
||||
]));
|
||||
console.log(`\n提示: 使用 --page N 查看下一页,--page-size N 调整每页数量`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'info': {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
const instances = await ecs.describeInstances(options.region, {
|
||||
instanceIds: JSON.stringify([options.id]),
|
||||
});
|
||||
if (instances.length === 0) {
|
||||
console.log('实例不存在');
|
||||
process.exit(1);
|
||||
}
|
||||
const inst = instances[0];
|
||||
console.log('\n实例详情:\n');
|
||||
console.log(` 实例ID: ${inst.instanceId}`);
|
||||
console.log(` 名称: ${inst.instanceName}`);
|
||||
console.log(` 状态: ${formatStatus(inst.status)}`);
|
||||
console.log(` 地域: ${inst.regionId}`);
|
||||
console.log(` 可用区: ${inst.zoneId}`);
|
||||
console.log(` 实例类型: ${inst.instanceType}`);
|
||||
console.log(` CPU: ${inst.cpu}核`);
|
||||
console.log(` 内存: ${inst.memory}MB`);
|
||||
console.log(` 操作系统: ${inst.osName}`);
|
||||
console.log(` 公网IP: ${inst.publicIpAddress.join(', ') || '无'}`);
|
||||
console.log(` 私网IP: ${inst.privateIpAddress.join(', ') || '无'}`);
|
||||
console.log(` 创建时间: ${inst.creationTime}`);
|
||||
console.log(` 到期时间: ${inst.expiredTime || '按量付费'}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'start': {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在启动实例 ${options.id}...`);
|
||||
const result = await ecs.startInstance(options.region, options.id);
|
||||
console.log(`✓ 启动成功 (RequestId: ${result.requestId})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'stop': {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在停止实例 ${options.id}...`);
|
||||
const result = await ecs.stopInstance(options.region, options.id, options.force);
|
||||
console.log(`✓ 停止成功 (RequestId: ${result.requestId})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'restart': {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在重启实例 ${options.id}...`);
|
||||
const result = await ecs.rebootInstance(options.region, options.id, options.force);
|
||||
console.log(`✓ 重启成功 (RequestId: ${result.requestId})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'monitor': {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 解析指标列表,默认为CPU和内存
|
||||
const metrics = options.metrics ? options.metrics.split(',') : ['CPU', 'Memory'];
|
||||
const period = parseInt(options.period) || 300; // 默认5分钟粒度
|
||||
const minutes = parseInt(options.minutes) || 60; // 默认查询最近60分钟
|
||||
|
||||
// 计算时间范围
|
||||
const endTime = new Date();
|
||||
const startTime = new Date(endTime.getTime() - minutes * 60 * 1000);
|
||||
|
||||
console.log(`\n正在查询实例 ${options.id} 的监控数据...`);
|
||||
console.log(`指标: ${metrics.join(', ')}`);
|
||||
console.log(`时间范围: ${startTime.toISOString()} ~ ${endTime.toISOString()}`);
|
||||
console.log('');
|
||||
|
||||
const monitorData = await ecs.describeInstanceMonitorData(
|
||||
options.region,
|
||||
options.id,
|
||||
period,
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString()
|
||||
);
|
||||
|
||||
if (monitorData.length === 0) {
|
||||
console.log('暂无监控数据(实例可能已停止或刚启动)');
|
||||
} else {
|
||||
// 格式化显示监控数据
|
||||
const columns = [{ key: 'timestamp', header: '时间' }];
|
||||
if (metrics.includes('CPU')) columns.push({ key: 'cpu', header: 'CPU(%)' });
|
||||
if (metrics.includes('Memory')) columns.push({ key: 'memory', header: '内存(%)' });
|
||||
if (metrics.includes('InternetIn')) columns.push({ key: 'internetIn', header: '公网入(Kbps)' });
|
||||
if (metrics.includes('InternetOut')) columns.push({ key: 'internetOut', header: '公网出(Kbps)' });
|
||||
if (metrics.includes('IntranetIn')) columns.push({ key: 'intranetIn', header: '内网入(Kbps)' });
|
||||
if (metrics.includes('IntranetOut')) columns.push({ key: 'intranetOut', header: '内网出(Kbps)' });
|
||||
|
||||
// 格式化时间戳和数据
|
||||
const formattedData = monitorData.map(d => ({
|
||||
timestamp: new Date(d.timestamp).toLocaleString('zh-CN'),
|
||||
cpu: d.cpu !== undefined ? d.cpu.toFixed(2) : '-',
|
||||
memory: d.memory !== undefined ? d.memory.toFixed(2) : '-',
|
||||
internetIn: d.internetIn !== undefined ? d.internetIn.toFixed(2) : '-',
|
||||
internetOut: d.internetOut !== undefined ? d.internetOut.toFixed(2) : '-',
|
||||
intranetIn: d.intranetIn !== undefined ? d.intranetIn.toFixed(2) : '-',
|
||||
intranetOut: d.intranetOut !== undefined ? d.intranetOut.toFixed(2) : '-',
|
||||
}));
|
||||
|
||||
console.log(formatTable(formattedData, columns));
|
||||
console.log(`\n共 ${monitorData.length} 个数据点`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'snapshot': {
|
||||
const subCommand = args[1];
|
||||
if (subCommand === 'list') {
|
||||
if (!options.region || !options.id) {
|
||||
console.error('错误: 请提供 --region 和 --id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
// 查询实例的快照列表
|
||||
const snapshots = await ecs.describeSnapshots(options.region, options.id, null);
|
||||
console.log(`\n实例 ${options.id} 的快照列表:\n`);
|
||||
if (snapshots.length === 0) {
|
||||
console.log('暂无快照');
|
||||
} else {
|
||||
console.log(formatTable(snapshots.map(snap => ({
|
||||
...snap,
|
||||
status: snap.status === 'accomplished' ? '已完成' : snap.status,
|
||||
})), [
|
||||
{ key: 'snapshotId', header: '快照ID' },
|
||||
{ key: 'snapshotName', header: '名称' },
|
||||
{ key: 'status', header: '状态' },
|
||||
{ key: 'progress', header: '进度' },
|
||||
{ key: 'creationTime', header: '创建时间' },
|
||||
]));
|
||||
}
|
||||
} else if (subCommand === 'create') {
|
||||
if (!options.region || !options['disk-id'] || !options.name) {
|
||||
console.error('错误: 请提供 --region, --disk-id 和 --name 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在创建快照 ${options.name}...`);
|
||||
const result = await ecs.createSnapshot(options.region, options['disk-id'], options.name, options.description || '');
|
||||
console.log(`✓ 快照创建成功 (SnapshotId: ${result.snapshotId})`);
|
||||
} else if (subCommand === 'rollback') {
|
||||
if (!options.region || !options['disk-id'] || !options['snapshot-id']) {
|
||||
console.error('错误: 请提供 --region, --disk-id 和 --snapshot-id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在回滚快照 ${options['snapshot-id']}...`);
|
||||
const result = await ecs.resetDisk(options.region, options['disk-id'], options['snapshot-id']);
|
||||
console.log(`✓ 回滚成功 (RequestId: ${result.requestId})`);
|
||||
} else {
|
||||
console.error('未知的 snapshot 子命令');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'security-group': {
|
||||
const subCommand = args[1];
|
||||
if (subCommand === 'list') {
|
||||
if (!options.region) {
|
||||
console.error('错误: 请提供 --region 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
const groups = await ecs.describeSecurityGroups(options.region);
|
||||
console.log(`\n地域 ${options.region} 的安全组列表:\n`);
|
||||
if (groups.length === 0) {
|
||||
console.log('暂无安全组');
|
||||
} else {
|
||||
console.log(formatTable(groups, [
|
||||
{ key: 'securityGroupId', header: '安全组ID' },
|
||||
{ key: 'securityGroupName', header: '名称' },
|
||||
{ key: 'description', header: '描述' },
|
||||
]));
|
||||
}
|
||||
} else if (subCommand === 'rules') {
|
||||
if (!options.region || !options['group-id']) {
|
||||
console.error('错误: 请提供 --region 和 --group-id 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
const rules = await ecs.describeSecurityGroupAttribute(options.region, options['group-id']);
|
||||
console.log(`\n安全组 ${options['group-id']} 的入方向规则:\n`);
|
||||
if (rules.length === 0) {
|
||||
console.log('暂无规则');
|
||||
} else {
|
||||
console.log(formatTable(rules.map(rule => ({
|
||||
...rule,
|
||||
policy: rule.policy === 'accept' ? '允许' : '拒绝',
|
||||
})), [
|
||||
{ key: 'ipProtocol', header: '协议' },
|
||||
{ key: 'portRange', header: '端口' },
|
||||
{ key: 'sourceCidrIp', header: '源IP' },
|
||||
{ key: 'policy', header: '策略' },
|
||||
{ key: 'description', header: '描述' },
|
||||
]));
|
||||
}
|
||||
} else if (subCommand === 'add') {
|
||||
if (!options.region || !options['group-id'] || !options.port) {
|
||||
console.error('错误: 请提供 --region, --group-id 和 --port 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在添加安全组规则 (端口: ${options.port})...`);
|
||||
const result = await ecs.authorizeSecurityGroup(
|
||||
options.region,
|
||||
options['group-id'],
|
||||
options.protocol || 'tcp',
|
||||
options.port,
|
||||
options.cidr || '0.0.0.0/0',
|
||||
options.description || ''
|
||||
);
|
||||
console.log(`✓ 规则添加成功 (RequestId: ${result.requestId})`);
|
||||
} else if (subCommand === 'remove') {
|
||||
if (!options.region || !options['group-id'] || !options.port) {
|
||||
console.error('错误: 请提供 --region, --group-id 和 --port 参数');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`正在删除安全组规则 (端口: ${options.port})...`);
|
||||
const result = await ecs.revokeSecurityGroup(
|
||||
options.region,
|
||||
options['group-id'],
|
||||
options.protocol || 'tcp',
|
||||
options.port,
|
||||
options.cidr || '0.0.0.0/0'
|
||||
);
|
||||
console.log(`✓ 规则删除成功 (RequestId: ${result.requestId})`);
|
||||
} else {
|
||||
console.error('未知的安全组子命令');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.error(`未知命令: ${command}`);
|
||||
showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`\n错误: ${error.message}`);
|
||||
if (error.message.includes('配置文件')) {
|
||||
console.error('\n请先运行设置脚本:');
|
||||
console.error(' ./scripts/setup.sh --access-key-id YOUR_ID --access-key-secret YOUR_SECRET');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
x
Reference in New Issue
Block a user