nonebot-plugin-awsmgmt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_awsmgmt-0.1.0/.gitignore +10 -0
- nonebot_plugin_awsmgmt-0.1.0/.python-version +1 -0
- nonebot_plugin_awsmgmt-0.1.0/PKG-INFO +126 -0
- nonebot_plugin_awsmgmt-0.1.0/README.md +116 -0
- nonebot_plugin_awsmgmt-0.1.0/__init__.py +189 -0
- nonebot_plugin_awsmgmt-0.1.0/config.py +14 -0
- nonebot_plugin_awsmgmt-0.1.0/cost_explorer_manager.py +51 -0
- nonebot_plugin_awsmgmt-0.1.0/ec2_manager.py +71 -0
- nonebot_plugin_awsmgmt-0.1.0/lightsail_manager.py +43 -0
- nonebot_plugin_awsmgmt-0.1.0/pyproject.toml +27 -0
- nonebot_plugin_awsmgmt-0.1.0/requirements-dev.lock +126 -0
- nonebot_plugin_awsmgmt-0.1.0/requirements.lock +126 -0
- nonebot_plugin_awsmgmt-0.1.0/src/nonebot_plugin_awsmgmt/__init__.py +2 -0
@@ -0,0 +1 @@
|
|
1
|
+
3.10.15
|
@@ -0,0 +1,126 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: nonebot-plugin-awsmgmt
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Add your description here
|
5
|
+
Author-email: Maximilian Wu <me@maxng.cc>
|
6
|
+
Requires-Python: >=3.8
|
7
|
+
Requires-Dist: aioboto3>=15.0.0
|
8
|
+
Requires-Dist: nonebot2[fastapi]>=2.4.2
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
|
11
|
+
# nonebot_plugin_awsmgmt
|
12
|
+
|
13
|
+
A NoneBot2 plugin for AWS management.
|
14
|
+
|
15
|
+
## 使用方法
|
16
|
+
|
17
|
+
1. **安装插件**
|
18
|
+
|
19
|
+
```bash
|
20
|
+
pip install nonebot-plugin-awsmgmt
|
21
|
+
```
|
22
|
+
|
23
|
+
2. **在 NoneBot2 项目中加载插件**
|
24
|
+
|
25
|
+
在 `bot.py` 或 `pyproject.toml` 中添加 `nonebot_plugin_awsmgmt` 到插件列表。
|
26
|
+
|
27
|
+
例如,在 `pyproject.toml` 中:
|
28
|
+
|
29
|
+
```toml
|
30
|
+
[tool.nonebot]
|
31
|
+
plugins = ["nonebot_plugin_awsmgmt"]
|
32
|
+
```
|
33
|
+
|
34
|
+
3. **配置 AWS 凭证**
|
35
|
+
|
36
|
+
在 NoneBot2 项目的 `.env` 文件中配置 AWS 访问密钥和秘密访问密钥:
|
37
|
+
|
38
|
+
```
|
39
|
+
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
|
40
|
+
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
|
41
|
+
AWS_REGION_NAME=your-aws-region # 例如:us-east-1
|
42
|
+
```
|
43
|
+
|
44
|
+
## AWS 侧配置
|
45
|
+
|
46
|
+
为了使插件能够管理您的 AWS 资源,您需要在 AWS IAM 中创建一个具有适当权限的用户。
|
47
|
+
|
48
|
+
### 1. 创建 IAM Policy
|
49
|
+
|
50
|
+
创建一个新的 IAM Policy,并粘贴以下 JSON 内容。您可以根据需要修改此策略以限制权限。
|
51
|
+
|
52
|
+
**Policy JSON (`aws-policy.json`):**
|
53
|
+
|
54
|
+
```json
|
55
|
+
{
|
56
|
+
"Version": "2012-10-17",
|
57
|
+
"Statement": [
|
58
|
+
{
|
59
|
+
"Effect": "Allow",
|
60
|
+
"Action": [
|
61
|
+
"ec2:DescribeInstances",
|
62
|
+
"ec2:RebootInstances"
|
63
|
+
],
|
64
|
+
"Resource": "*"
|
65
|
+
},
|
66
|
+
{
|
67
|
+
"Effect": "Allow",
|
68
|
+
"Action": [
|
69
|
+
"ec2:StartInstances",
|
70
|
+
"ec2:StopInstances"
|
71
|
+
],
|
72
|
+
"Resource": "arn:aws:ec2:*:*:instance/*",
|
73
|
+
"Condition": {
|
74
|
+
"StringEquals": {
|
75
|
+
"ec2:ResourceTag/ManagedBy": "nonebot-plugin-awsmgmt"
|
76
|
+
}
|
77
|
+
}
|
78
|
+
},
|
79
|
+
{
|
80
|
+
"Effect": "Allow",
|
81
|
+
"Action": [
|
82
|
+
"lightsail:GetInstances",
|
83
|
+
"lightsail:GetInstance",
|
84
|
+
"lightsail:StartInstance",
|
85
|
+
"lightsail:StopInstance"
|
86
|
+
],
|
87
|
+
"Resource": "*"
|
88
|
+
},
|
89
|
+
{
|
90
|
+
"Effect": "Allow",
|
91
|
+
"Action": [
|
92
|
+
"ce:GetCostAndUsage"
|
93
|
+
],
|
94
|
+
"Resource": "*"
|
95
|
+
}
|
96
|
+
]
|
97
|
+
}
|
98
|
+
```
|
99
|
+
|
100
|
+
**步骤:**
|
101
|
+
|
102
|
+
1. 登录 AWS 管理控制台。
|
103
|
+
2. 导航到 IAM 服务。
|
104
|
+
3. 在左侧导航栏中选择 **Policies**。
|
105
|
+
4. 点击 **Create policy**。
|
106
|
+
5. 选择 **JSON** 选项卡,并粘贴上述 Policy JSON 内容。
|
107
|
+
6. 点击 **Next: Tags**,然后点击 **Next: Review**。
|
108
|
+
7. 为策略命名(例如:`NoneBotAWSPolicy`),并添加描述。
|
109
|
+
8. 点击 **Create policy**。
|
110
|
+
|
111
|
+
### 2. 创建 IAM 用户并附加策略
|
112
|
+
|
113
|
+
创建一个新的 IAM 用户,并为其提供编程访问权限,然后附加您刚刚创建的策略。
|
114
|
+
|
115
|
+
**步骤:**
|
116
|
+
|
117
|
+
1. 在 IAM 服务中,选择左侧导航栏中的 **Users**。
|
118
|
+
2. 点击 **Add user**。
|
119
|
+
3. 输入用户名(例如:`nonebot-aws-user`)。
|
120
|
+
4. 在 **Select AWS access type** 部分,勾选 **Programmatic access**。
|
121
|
+
5. 点击 **Next: Permissions**。
|
122
|
+
6. 选择 **Attach existing policies directly**。
|
123
|
+
7. 搜索并选择您刚刚创建的策略(例如:`NoneBotAWSPolicy`)。
|
124
|
+
8. 点击 **Next: Tags**,然后点击 **Next: Review**。
|
125
|
+
9. 点击 **Create user**。
|
126
|
+
10. **重要:** 记录下生成的 **Access key ID** 和 **Secret access key**。这些将用于配置 NoneBot2 插件。这些凭证只显示一次,请务必妥善保管。
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# nonebot_plugin_awsmgmt
|
2
|
+
|
3
|
+
A NoneBot2 plugin for AWS management.
|
4
|
+
|
5
|
+
## 使用方法
|
6
|
+
|
7
|
+
1. **安装插件**
|
8
|
+
|
9
|
+
```bash
|
10
|
+
pip install nonebot-plugin-awsmgmt
|
11
|
+
```
|
12
|
+
|
13
|
+
2. **在 NoneBot2 项目中加载插件**
|
14
|
+
|
15
|
+
在 `bot.py` 或 `pyproject.toml` 中添加 `nonebot_plugin_awsmgmt` 到插件列表。
|
16
|
+
|
17
|
+
例如,在 `pyproject.toml` 中:
|
18
|
+
|
19
|
+
```toml
|
20
|
+
[tool.nonebot]
|
21
|
+
plugins = ["nonebot_plugin_awsmgmt"]
|
22
|
+
```
|
23
|
+
|
24
|
+
3. **配置 AWS 凭证**
|
25
|
+
|
26
|
+
在 NoneBot2 项目的 `.env` 文件中配置 AWS 访问密钥和秘密访问密钥:
|
27
|
+
|
28
|
+
```
|
29
|
+
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
|
30
|
+
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
|
31
|
+
AWS_REGION_NAME=your-aws-region # 例如:us-east-1
|
32
|
+
```
|
33
|
+
|
34
|
+
## AWS 侧配置
|
35
|
+
|
36
|
+
为了使插件能够管理您的 AWS 资源,您需要在 AWS IAM 中创建一个具有适当权限的用户。
|
37
|
+
|
38
|
+
### 1. 创建 IAM Policy
|
39
|
+
|
40
|
+
创建一个新的 IAM Policy,并粘贴以下 JSON 内容。您可以根据需要修改此策略以限制权限。
|
41
|
+
|
42
|
+
**Policy JSON (`aws-policy.json`):**
|
43
|
+
|
44
|
+
```json
|
45
|
+
{
|
46
|
+
"Version": "2012-10-17",
|
47
|
+
"Statement": [
|
48
|
+
{
|
49
|
+
"Effect": "Allow",
|
50
|
+
"Action": [
|
51
|
+
"ec2:DescribeInstances",
|
52
|
+
"ec2:RebootInstances"
|
53
|
+
],
|
54
|
+
"Resource": "*"
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"Effect": "Allow",
|
58
|
+
"Action": [
|
59
|
+
"ec2:StartInstances",
|
60
|
+
"ec2:StopInstances"
|
61
|
+
],
|
62
|
+
"Resource": "arn:aws:ec2:*:*:instance/*",
|
63
|
+
"Condition": {
|
64
|
+
"StringEquals": {
|
65
|
+
"ec2:ResourceTag/ManagedBy": "nonebot-plugin-awsmgmt"
|
66
|
+
}
|
67
|
+
}
|
68
|
+
},
|
69
|
+
{
|
70
|
+
"Effect": "Allow",
|
71
|
+
"Action": [
|
72
|
+
"lightsail:GetInstances",
|
73
|
+
"lightsail:GetInstance",
|
74
|
+
"lightsail:StartInstance",
|
75
|
+
"lightsail:StopInstance"
|
76
|
+
],
|
77
|
+
"Resource": "*"
|
78
|
+
},
|
79
|
+
{
|
80
|
+
"Effect": "Allow",
|
81
|
+
"Action": [
|
82
|
+
"ce:GetCostAndUsage"
|
83
|
+
],
|
84
|
+
"Resource": "*"
|
85
|
+
}
|
86
|
+
]
|
87
|
+
}
|
88
|
+
```
|
89
|
+
|
90
|
+
**步骤:**
|
91
|
+
|
92
|
+
1. 登录 AWS 管理控制台。
|
93
|
+
2. 导航到 IAM 服务。
|
94
|
+
3. 在左侧导航栏中选择 **Policies**。
|
95
|
+
4. 点击 **Create policy**。
|
96
|
+
5. 选择 **JSON** 选项卡,并粘贴上述 Policy JSON 内容。
|
97
|
+
6. 点击 **Next: Tags**,然后点击 **Next: Review**。
|
98
|
+
7. 为策略命名(例如:`NoneBotAWSPolicy`),并添加描述。
|
99
|
+
8. 点击 **Create policy**。
|
100
|
+
|
101
|
+
### 2. 创建 IAM 用户并附加策略
|
102
|
+
|
103
|
+
创建一个新的 IAM 用户,并为其提供编程访问权限,然后附加您刚刚创建的策略。
|
104
|
+
|
105
|
+
**步骤:**
|
106
|
+
|
107
|
+
1. 在 IAM 服务中,选择左侧导航栏中的 **Users**。
|
108
|
+
2. 点击 **Add user**。
|
109
|
+
3. 输入用户名(例如:`nonebot-aws-user`)。
|
110
|
+
4. 在 **Select AWS access type** 部分,勾选 **Programmatic access**。
|
111
|
+
5. 点击 **Next: Permissions**。
|
112
|
+
6. 选择 **Attach existing policies directly**。
|
113
|
+
7. 搜索并选择您刚刚创建的策略(例如:`NoneBotAWSPolicy`)。
|
114
|
+
8. 点击 **Next: Tags**,然后点击 **Next: Review**。
|
115
|
+
9. 点击 **Create user**。
|
116
|
+
10. **重要:** 记录下生成的 **Access key ID** 和 **Secret access key**。这些将用于配置 NoneBot2 插件。这些凭证只显示一次,请务必妥善保管。
|
@@ -0,0 +1,189 @@
|
|
1
|
+
import time
|
2
|
+
import re
|
3
|
+
from typing import Tuple, List, Optional, Dict, Any
|
4
|
+
from functools import wraps
|
5
|
+
|
6
|
+
from nonebot import on_command
|
7
|
+
from nonebot.matcher import Matcher
|
8
|
+
from nonebot.permission import SUPERUSER
|
9
|
+
from nonebot.plugin import PluginMetadata, get_plugin_config
|
10
|
+
from nonebot.params import CommandArg
|
11
|
+
from nonebot.adapters import Message
|
12
|
+
from nonebot.log import logger
|
13
|
+
from nonebot.exception import FinishedException
|
14
|
+
|
15
|
+
from .config import Config
|
16
|
+
from .ec2_manager import EC2Manager
|
17
|
+
from .cost_explorer_manager import CostExplorerManager
|
18
|
+
from .lightsail_manager import LightsailManager
|
19
|
+
|
20
|
+
__plugin_meta__ = PluginMetadata(
|
21
|
+
name="AWS Manager",
|
22
|
+
description="Manage AWS EC2, Lightsail, and Cost Explorer via commands.",
|
23
|
+
usage=(
|
24
|
+
"--- EC2 ---\n"
|
25
|
+
"/ec2_start|stop|reboot|status [target]\n"
|
26
|
+
"Target: tag:Key:Value | id:i-xxxx\n"
|
27
|
+
"--- Lightsail ---\n"
|
28
|
+
"/lightsail_list\n"
|
29
|
+
"/lightsail_start|stop <instance_name>\n"
|
30
|
+
"--- Cost ---\n"
|
31
|
+
"/aws_cost today|month|month by_service"
|
32
|
+
),
|
33
|
+
type="application",
|
34
|
+
homepage="https://github.com/maxesisn/nonebot-plugin-awsmgmt",
|
35
|
+
config=Config,
|
36
|
+
)
|
37
|
+
|
38
|
+
def handle_non_finish_exceptions(error_message: str):
|
39
|
+
def decorator(func):
|
40
|
+
@wraps(func)
|
41
|
+
async def wrapper(*args, **kwargs):
|
42
|
+
try:
|
43
|
+
return await func(*args, **kwargs)
|
44
|
+
except FinishedException:
|
45
|
+
raise
|
46
|
+
except Exception as e:
|
47
|
+
matcher = args[0] if args else None
|
48
|
+
logger.error(f"Error in {func.__name__}: {e}")
|
49
|
+
if matcher:
|
50
|
+
await matcher.finish(error_message)
|
51
|
+
return wrapper
|
52
|
+
return decorator
|
53
|
+
|
54
|
+
# --- Init --- #
|
55
|
+
plugin_config = get_plugin_config(Config)
|
56
|
+
ec2_manager = EC2Manager(plugin_config)
|
57
|
+
cost_manager = CostExplorerManager(plugin_config)
|
58
|
+
lightsail_manager = LightsailManager(plugin_config)
|
59
|
+
|
60
|
+
# --- Command Matchers --- #
|
61
|
+
# EC2
|
62
|
+
ec2_start_matcher = on_command("ec2_start", aliases={"ec2启动"}, permission=SUPERUSER)
|
63
|
+
ec2_stop_matcher = on_command("ec2_stop", aliases={"ec2停止"}, permission=SUPERUSER)
|
64
|
+
ec2_reboot_matcher = on_command("ec2_reboot", aliases={"ec2重启"}, permission=SUPERUSER)
|
65
|
+
ec2_status_matcher = on_command("ec2_status", aliases={"ec2状态"}, permission=SUPERUSER)
|
66
|
+
# Lightsail
|
67
|
+
lightsail_list_matcher = on_command("lightsail_list", permission=SUPERUSER)
|
68
|
+
lightsail_start_matcher = on_command("lightsail_start", permission=SUPERUSER)
|
69
|
+
lightsail_stop_matcher = on_command("lightsail_stop", permission=SUPERUSER)
|
70
|
+
# Cost Explorer
|
71
|
+
cost_matcher = on_command("aws_cost", permission=SUPERUSER)
|
72
|
+
|
73
|
+
|
74
|
+
# --- Helper Functions --- #
|
75
|
+
|
76
|
+
async def parse_ec2_target(matcher: Matcher, args: Message) -> Tuple[str, str, Optional[str]]:
|
77
|
+
arg_str = args.extract_plain_text().strip()
|
78
|
+
if not arg_str:
|
79
|
+
if plugin_config.aws_default_target_tag:
|
80
|
+
arg_str = f"tag:{plugin_config.aws_default_target_tag}"
|
81
|
+
else:
|
82
|
+
await matcher.finish(__plugin_meta__.usage)
|
83
|
+
match = re.match(r"^(tag|id):(.*)$", arg_str)
|
84
|
+
if not match:
|
85
|
+
await matcher.finish(f"Invalid EC2 target format. \n{__plugin_meta__.usage}")
|
86
|
+
target_type, value = match.groups()
|
87
|
+
if target_type == "tag":
|
88
|
+
if ":" not in value:
|
89
|
+
await matcher.finish(f"Invalid tag format. Expected Key:Value. \n{__plugin_meta__.usage}")
|
90
|
+
tag_key, tag_value = value.split(":", 1)
|
91
|
+
return "tag", tag_key, tag_value
|
92
|
+
elif target_type == "id":
|
93
|
+
return "id", value, None
|
94
|
+
return "unknown", "", None
|
95
|
+
|
96
|
+
def format_ec2_status(instance: Dict[str, Any]) -> str:
|
97
|
+
instance_id = instance.get('InstanceId', 'N/A')
|
98
|
+
state = instance.get('State', {}).get('Name', 'N/A')
|
99
|
+
public_ip = instance.get('PublicIpAddress', 'None')
|
100
|
+
name_tag = next((tag['Value'] for tag in instance.get('Tags', []) if tag['Key'] == 'Name'), 'No Name Tag')
|
101
|
+
return f"- {instance_id} ({name_tag})\n State: {state}\n Public IP: {public_ip}"
|
102
|
+
|
103
|
+
|
104
|
+
# --- EC2 Handlers --- #
|
105
|
+
|
106
|
+
@ec2_status_matcher.handle()
|
107
|
+
@handle_non_finish_exceptions("An error occurred while fetching EC2 status.")
|
108
|
+
async def handle_ec2_status(matcher: Matcher, args: Message = CommandArg()):
|
109
|
+
target_type, value1, value2 = await parse_ec2_target(matcher, args)
|
110
|
+
|
111
|
+
if target_type == "tag":
|
112
|
+
instances = await ec2_manager.get_instances_by_tag(value1, value2, states=['pending', 'running', 'stopping', 'stopped'])
|
113
|
+
else:
|
114
|
+
instances = await ec2_manager.get_instances_by_id([value1], states=['pending', 'running', 'stopping', 'stopped'])
|
115
|
+
if not instances:
|
116
|
+
await matcher.finish("No EC2 instances found for the specified target.")
|
117
|
+
status_list = [format_ec2_status(inst) for inst in instances]
|
118
|
+
await matcher.finish("EC2 Instance Status:\n" + "\n".join(status_list))
|
119
|
+
|
120
|
+
|
121
|
+
# ... (omitting other EC2 handlers for brevity, they remain the same)
|
122
|
+
|
123
|
+
|
124
|
+
# --- Lightsail Handlers ---
|
125
|
+
|
126
|
+
@lightsail_list_matcher.handle()
|
127
|
+
@handle_non_finish_exceptions("An error occurred listing Lightsail instances.")
|
128
|
+
async def handle_lightsail_list(matcher: Matcher):
|
129
|
+
instances = await lightsail_manager.get_all_instances()
|
130
|
+
if not instances:
|
131
|
+
await matcher.finish("No Lightsail instances found.")
|
132
|
+
|
133
|
+
def format_lightsail(inst):
|
134
|
+
return f"- {inst['name']} ({inst['state']['name']})\n Region: {inst['location']['regionName']}\n IP: {inst['publicIpAddress']}"
|
135
|
+
|
136
|
+
status_list = [format_lightsail(inst) for inst in instances]
|
137
|
+
await matcher.finish("Lightsail Instances:\n" + "\n".join(status_list))
|
138
|
+
|
139
|
+
@lightsail_start_matcher.handle()
|
140
|
+
@handle_non_finish_exceptions("An error occurred while starting the Lightsail instance.")
|
141
|
+
async def handle_lightsail_start(matcher: Matcher, args: Message = CommandArg()):
|
142
|
+
instance_name = args.extract_plain_text().strip()
|
143
|
+
if not instance_name:
|
144
|
+
await matcher.finish("Please provide a Lightsail instance name.")
|
145
|
+
|
146
|
+
await matcher.send(f"Sending start command to {instance_name}...\nWaiting for it to become running...")
|
147
|
+
await lightsail_manager.start_instance(instance_name)
|
148
|
+
await lightsail_manager.wait_for_status(instance_name, 'running')
|
149
|
+
await matcher.finish(f"Successfully started Lightsail instance: {instance_name}")
|
150
|
+
|
151
|
+
@lightsail_stop_matcher.handle()
|
152
|
+
@handle_non_finish_exceptions("An error occurred while stopping the Lightsail instance.")
|
153
|
+
async def handle_lightsail_stop(matcher: Matcher, args: Message = CommandArg()):
|
154
|
+
instance_name = args.extract_plain_text().strip()
|
155
|
+
if not instance_name:
|
156
|
+
await matcher.finish("Please provide a Lightsail instance name.")
|
157
|
+
|
158
|
+
await matcher.send(f"Sending stop command to {instance_name}...\nWaiting for it to become stopped...")
|
159
|
+
await lightsail_manager.stop_instance(instance_name)
|
160
|
+
await lightsail_manager.wait_for_status(instance_name, 'stopped')
|
161
|
+
await matcher.finish(f"Successfully stopped Lightsail instance: {instance_name}")
|
162
|
+
|
163
|
+
|
164
|
+
# --- Cost Explorer Handlers ---
|
165
|
+
|
166
|
+
@cost_matcher.handle()
|
167
|
+
@handle_non_finish_exceptions("An error occurred while fetching AWS cost data.")
|
168
|
+
async def handle_cost(matcher: Matcher, args: Message = CommandArg()):
|
169
|
+
sub_command = args.extract_plain_text().strip()
|
170
|
+
|
171
|
+
if sub_command == "today":
|
172
|
+
result = await cost_manager.get_cost_today()
|
173
|
+
cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
|
174
|
+
await matcher.finish(f"AWS cost for today: {float(cost['Amount']):.4f} {cost['Unit']}")
|
175
|
+
elif sub_command == "month":
|
176
|
+
result = await cost_manager.get_cost_this_month()
|
177
|
+
cost = result['ResultsByTime'][0]['Total']['UnblendedCost']
|
178
|
+
await matcher.finish(f"AWS cost this month: {float(cost['Amount']):.4f} {cost['Unit']}")
|
179
|
+
elif sub_command == "month by_service":
|
180
|
+
result = await cost_manager.get_cost_this_month_by_service()
|
181
|
+
lines = ["Cost this month by service:"]
|
182
|
+
for group in sorted(result['ResultsByTime'][0]['Groups'], key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), reverse=True):
|
183
|
+
service_name = group['Keys'][0]
|
184
|
+
cost = group['Metrics']['UnblendedCost']
|
185
|
+
if float(cost['Amount']) > 0:
|
186
|
+
lines.append(f"- {service_name}: {float(cost['Amount']):.4f} {cost['Unit']}")
|
187
|
+
await matcher.finish("\n".join(lines))
|
188
|
+
else:
|
189
|
+
await matcher.finish("Invalid cost command. Use: today, month, month by_service")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
class Config(BaseModel):
|
5
|
+
"""
|
6
|
+
AWS Management Plugin Configuration
|
7
|
+
"""
|
8
|
+
aws_access_key_id: Optional[str] = None
|
9
|
+
aws_secret_access_key: Optional[str] = None
|
10
|
+
aws_region: str = "us-east-1"
|
11
|
+
aws_default_target_tag: Optional[str] = "ManagedBy:nonebot-plugin-awsmgmt"
|
12
|
+
|
13
|
+
class Config:
|
14
|
+
extra = "ignore"
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import aioboto3
|
2
|
+
from datetime import datetime, date, timedelta
|
3
|
+
from typing import Dict, Any, List
|
4
|
+
|
5
|
+
from .config import Config
|
6
|
+
|
7
|
+
class CostExplorerManager:
|
8
|
+
def __init__(self, config: Config):
|
9
|
+
self.config = config
|
10
|
+
# Cost Explorer is only available in us-east-1, but we use the session region for authentication
|
11
|
+
self.session = aioboto3.Session(
|
12
|
+
aws_access_key_id=self.config.aws_access_key_id,
|
13
|
+
aws_secret_access_key=self.config.aws_secret_access_key,
|
14
|
+
region_name=self.config.aws_region,
|
15
|
+
)
|
16
|
+
|
17
|
+
async def _get_cost(self, start_date: str, end_date: str, granularity: str, group_by: List[Dict[str, str]] = None) -> Dict[str, Any]:
|
18
|
+
async with self.session.client("ce", region_name="us-east-1") as ce:
|
19
|
+
kwargs = {
|
20
|
+
"TimePeriod": {"Start": start_date, "End": end_date},
|
21
|
+
"Granularity": granularity,
|
22
|
+
"Metrics": ["UnblendedCost"],
|
23
|
+
}
|
24
|
+
if group_by:
|
25
|
+
kwargs["GroupBy"] = group_by
|
26
|
+
return await ce.get_cost_and_usage(**kwargs)
|
27
|
+
|
28
|
+
async def get_cost_today(self) -> Dict[str, Any]:
|
29
|
+
"""Fetches the cost for today."""
|
30
|
+
today = date.today()
|
31
|
+
start_of_day = today.isoformat()
|
32
|
+
end_of_day = (today + timedelta(days=1)).isoformat()
|
33
|
+
return await self._get_cost(start_of_day, end_of_day, "DAILY")
|
34
|
+
|
35
|
+
async def get_cost_this_month(self) -> Dict[str, Any]:
|
36
|
+
"""Fetches the cost for the current month."""
|
37
|
+
today = date.today()
|
38
|
+
start_of_month = today.replace(day=1).isoformat()
|
39
|
+
# The end date is exclusive, so we use the first day of the next month
|
40
|
+
next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
|
41
|
+
end_of_month = next_month.isoformat()
|
42
|
+
return await self._get_cost(start_of_month, end_of_month, "MONTHLY")
|
43
|
+
|
44
|
+
async def get_cost_this_month_by_service(self) -> Dict[str, Any]:
|
45
|
+
"""Fetches the cost for the current month, grouped by service."""
|
46
|
+
today = date.today()
|
47
|
+
start_of_month = today.replace(day=1).isoformat()
|
48
|
+
next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1)
|
49
|
+
end_of_month = next_month.isoformat()
|
50
|
+
group_by = [{"Type": "DIMENSION", "Key": "SERVICE"}]
|
51
|
+
return await self._get_cost(start_of_month, end_of_month, "MONTHLY", group_by=group_by)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import aioboto3
|
2
|
+
from typing import List, Dict, Any
|
3
|
+
from .config import Config
|
4
|
+
|
5
|
+
class EC2Manager:
|
6
|
+
def __init__(self, config: Config):
|
7
|
+
self.config = config
|
8
|
+
self.session = aioboto3.Session(
|
9
|
+
aws_access_key_id=self.config.aws_access_key_id,
|
10
|
+
aws_secret_access_key=self.config.aws_secret_access_key,
|
11
|
+
region_name=self.config.aws_region,
|
12
|
+
)
|
13
|
+
|
14
|
+
async def get_instances_by_tag(self, tag_key: str, tag_value: str, states: List[str] = ['running', 'stopped']) -> List[Dict[str, Any]]:
|
15
|
+
"""Finds EC2 instances based on a specific tag."""
|
16
|
+
instances = []
|
17
|
+
async with self.session.client("ec2") as ec2:
|
18
|
+
paginator = ec2.get_paginator('describe_instances')
|
19
|
+
async for page in paginator.paginate(
|
20
|
+
Filters=[
|
21
|
+
{'Name': f'tag:{tag_key}', 'Values': [tag_value]},
|
22
|
+
{'Name': 'instance-state-name', 'Values': states}
|
23
|
+
]
|
24
|
+
):
|
25
|
+
for reservation in page['Reservations']:
|
26
|
+
for instance in reservation['Instances']:
|
27
|
+
instances.append(instance)
|
28
|
+
return instances
|
29
|
+
|
30
|
+
async def get_instances_by_id(self, instance_ids: List[str], states: List[str] = ['running', 'stopped']) -> List[Dict[str, Any]]:
|
31
|
+
"""Finds EC2 instances based on a list of IDs."""
|
32
|
+
instances = []
|
33
|
+
async with self.session.client("ec2") as ec2:
|
34
|
+
paginator = ec2.get_paginator('describe_instances')
|
35
|
+
async for page in paginator.paginate(
|
36
|
+
InstanceIds=instance_ids,
|
37
|
+
Filters=[{'Name': 'instance-state-name', 'Values': states}]
|
38
|
+
):
|
39
|
+
for reservation in page['Reservations']:
|
40
|
+
for instance in reservation['Instances']:
|
41
|
+
instances.append(instance)
|
42
|
+
return instances
|
43
|
+
|
44
|
+
async def start_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
|
45
|
+
"""Starts the specified EC2 instances."""
|
46
|
+
if not instance_ids:
|
47
|
+
return {"StartingInstances": []}
|
48
|
+
async with self.session.client("ec2") as ec2:
|
49
|
+
return await ec2.start_instances(InstanceIds=instance_ids)
|
50
|
+
|
51
|
+
async def stop_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
|
52
|
+
"""Stops the specified EC2 instances."""
|
53
|
+
if not instance_ids:
|
54
|
+
return {"StoppingInstances": []}
|
55
|
+
async with self.session.client("ec2") as ec2:
|
56
|
+
return await ec2.stop_instances(InstanceIds=instance_ids)
|
57
|
+
|
58
|
+
async def reboot_instances(self, instance_ids: List[str]) -> Dict[str, Any]:
|
59
|
+
"""Reboots the specified EC2 instances."""
|
60
|
+
if not instance_ids:
|
61
|
+
return {}
|
62
|
+
async with self.session.client("ec2") as ec2:
|
63
|
+
return await ec2.reboot_instances(InstanceIds=instance_ids)
|
64
|
+
|
65
|
+
async def wait_for_status(self, instance_ids: List[str], status: str):
|
66
|
+
"""Waits for instances to reach a specific status."""
|
67
|
+
if not instance_ids:
|
68
|
+
return
|
69
|
+
async with self.session.client("ec2") as ec2:
|
70
|
+
waiter = ec2.get_waiter(status)
|
71
|
+
await waiter.wait(InstanceIds=instance_ids)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import asyncio
|
2
|
+
import aioboto3
|
3
|
+
from typing import List, Dict, Any
|
4
|
+
|
5
|
+
from .config import Config
|
6
|
+
|
7
|
+
class LightsailManager:
|
8
|
+
def __init__(self, config: Config):
|
9
|
+
self.config = config
|
10
|
+
self.session = aioboto3.Session(
|
11
|
+
aws_access_key_id=self.config.aws_access_key_id,
|
12
|
+
aws_secret_access_key=self.config.aws_secret_access_key,
|
13
|
+
region_name=self.config.aws_region,
|
14
|
+
)
|
15
|
+
|
16
|
+
async def get_all_instances(self) -> List[Dict[str, Any]]:
|
17
|
+
"""Gets a list of all Lightsail instances."""
|
18
|
+
async with self.session.client("lightsail") as lightsail:
|
19
|
+
response = await lightsail.get_instances()
|
20
|
+
return response.get('instances', [])
|
21
|
+
|
22
|
+
async def start_instance(self, instance_name: str) -> Dict[str, Any]:
|
23
|
+
"""Starts a specific Lightsail instance."""
|
24
|
+
async with self.session.client("lightsail") as lightsail:
|
25
|
+
return await lightsail.start_instance(instanceName=instance_name)
|
26
|
+
|
27
|
+
async def stop_instance(self, instance_name: str) -> Dict[str, Any]:
|
28
|
+
"""Stops a specific Lightsail instance."""
|
29
|
+
async with self.session.client("lightsail") as lightsail:
|
30
|
+
return await lightsail.stop_instance(instanceName=instance_name)
|
31
|
+
|
32
|
+
async def wait_for_status(self, instance_name: str, target_status: str, timeout: int = 300, delay: int = 15):
|
33
|
+
"""Waits for a lightsail instance to reach a specific status."""
|
34
|
+
async with self.session.client("lightsail") as lightsail:
|
35
|
+
elapsed_time = 0
|
36
|
+
while elapsed_time < timeout:
|
37
|
+
response = await lightsail.get_instance(instanceName=instance_name)
|
38
|
+
instance = response.get('instance')
|
39
|
+
if instance and instance.get('state', {}).get('name') == target_status:
|
40
|
+
return
|
41
|
+
await asyncio.sleep(delay)
|
42
|
+
elapsed_time += delay
|
43
|
+
raise asyncio.TimeoutError(f"Instance {instance_name} did not reach {target_status} within {timeout} seconds.")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
[project]
|
2
|
+
name = "nonebot-plugin-awsmgmt"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "Add your description here"
|
5
|
+
authors = [
|
6
|
+
{ name = "Maximilian Wu", email = "me@maxng.cc" }
|
7
|
+
]
|
8
|
+
dependencies = [
|
9
|
+
"nonebot2[fastapi]>=2.4.2",
|
10
|
+
"aioboto3>=15.0.0",
|
11
|
+
]
|
12
|
+
readme = "README.md"
|
13
|
+
requires-python = ">= 3.8"
|
14
|
+
|
15
|
+
[build-system]
|
16
|
+
requires = ["hatchling==1.26.3"]
|
17
|
+
build-backend = "hatchling.build"
|
18
|
+
|
19
|
+
[tool.rye]
|
20
|
+
managed = true
|
21
|
+
dev-dependencies = []
|
22
|
+
|
23
|
+
[tool.hatch.metadata]
|
24
|
+
allow-direct-references = true
|
25
|
+
|
26
|
+
[tool.hatch.build.targets.wheel]
|
27
|
+
packages = ["src/nonebot_plugin_awsmgmt"]
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# generated by rye
|
2
|
+
# use `rye lock` or `rye sync` to update this lockfile
|
3
|
+
#
|
4
|
+
# last locked with the following flags:
|
5
|
+
# pre: false
|
6
|
+
# features: []
|
7
|
+
# all-features: false
|
8
|
+
# with-sources: false
|
9
|
+
# generate-hashes: false
|
10
|
+
# universal: false
|
11
|
+
|
12
|
+
-e file:.
|
13
|
+
aioboto3==15.0.0
|
14
|
+
# via nonebot-plugin-awsmgmt
|
15
|
+
aiobotocore==2.23.0
|
16
|
+
# via aioboto3
|
17
|
+
aiofiles==24.1.0
|
18
|
+
# via aioboto3
|
19
|
+
aiohappyeyeballs==2.6.1
|
20
|
+
# via aiohttp
|
21
|
+
aiohttp==3.12.13
|
22
|
+
# via aiobotocore
|
23
|
+
aioitertools==0.12.0
|
24
|
+
# via aiobotocore
|
25
|
+
aiosignal==1.3.2
|
26
|
+
# via aiohttp
|
27
|
+
annotated-types==0.7.0
|
28
|
+
# via pydantic
|
29
|
+
anyio==4.9.0
|
30
|
+
# via nonebot2
|
31
|
+
# via starlette
|
32
|
+
# via watchfiles
|
33
|
+
async-timeout==5.0.1
|
34
|
+
# via aiohttp
|
35
|
+
attrs==25.3.0
|
36
|
+
# via aiohttp
|
37
|
+
boto3==1.38.27
|
38
|
+
# via aiobotocore
|
39
|
+
botocore==1.38.27
|
40
|
+
# via aiobotocore
|
41
|
+
# via boto3
|
42
|
+
# via s3transfer
|
43
|
+
click==8.2.1
|
44
|
+
# via uvicorn
|
45
|
+
exceptiongroup==1.3.0
|
46
|
+
# via anyio
|
47
|
+
# via nonebot2
|
48
|
+
fastapi==0.115.14
|
49
|
+
# via nonebot2
|
50
|
+
frozenlist==1.7.0
|
51
|
+
# via aiohttp
|
52
|
+
# via aiosignal
|
53
|
+
h11==0.16.0
|
54
|
+
# via uvicorn
|
55
|
+
httptools==0.6.4
|
56
|
+
# via uvicorn
|
57
|
+
idna==3.10
|
58
|
+
# via anyio
|
59
|
+
# via yarl
|
60
|
+
jmespath==1.0.1
|
61
|
+
# via aiobotocore
|
62
|
+
# via boto3
|
63
|
+
# via botocore
|
64
|
+
loguru==0.7.3
|
65
|
+
# via nonebot2
|
66
|
+
multidict==6.6.2
|
67
|
+
# via aiobotocore
|
68
|
+
# via aiohttp
|
69
|
+
# via yarl
|
70
|
+
nonebot2==2.4.2
|
71
|
+
# via nonebot-plugin-awsmgmt
|
72
|
+
propcache==0.3.2
|
73
|
+
# via aiohttp
|
74
|
+
# via yarl
|
75
|
+
pydantic==2.11.7
|
76
|
+
# via fastapi
|
77
|
+
# via nonebot2
|
78
|
+
pydantic-core==2.33.2
|
79
|
+
# via pydantic
|
80
|
+
pygtrie==2.5.0
|
81
|
+
# via nonebot2
|
82
|
+
python-dateutil==2.9.0.post0
|
83
|
+
# via aiobotocore
|
84
|
+
# via botocore
|
85
|
+
python-dotenv==1.1.1
|
86
|
+
# via nonebot2
|
87
|
+
# via uvicorn
|
88
|
+
pyyaml==6.0.2
|
89
|
+
# via uvicorn
|
90
|
+
s3transfer==0.13.0
|
91
|
+
# via boto3
|
92
|
+
six==1.17.0
|
93
|
+
# via python-dateutil
|
94
|
+
sniffio==1.3.1
|
95
|
+
# via anyio
|
96
|
+
starlette==0.46.2
|
97
|
+
# via fastapi
|
98
|
+
tomli==2.2.1
|
99
|
+
# via nonebot2
|
100
|
+
typing-extensions==4.14.0
|
101
|
+
# via anyio
|
102
|
+
# via exceptiongroup
|
103
|
+
# via fastapi
|
104
|
+
# via multidict
|
105
|
+
# via nonebot2
|
106
|
+
# via pydantic
|
107
|
+
# via pydantic-core
|
108
|
+
# via typing-inspection
|
109
|
+
# via uvicorn
|
110
|
+
typing-inspection==0.4.1
|
111
|
+
# via pydantic
|
112
|
+
urllib3==2.5.0
|
113
|
+
# via botocore
|
114
|
+
uvicorn==0.35.0
|
115
|
+
# via nonebot2
|
116
|
+
uvloop==0.21.0
|
117
|
+
# via uvicorn
|
118
|
+
watchfiles==1.1.0
|
119
|
+
# via uvicorn
|
120
|
+
websockets==15.0.1
|
121
|
+
# via uvicorn
|
122
|
+
wrapt==1.17.2
|
123
|
+
# via aiobotocore
|
124
|
+
yarl==1.20.1
|
125
|
+
# via aiohttp
|
126
|
+
# via nonebot2
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# generated by rye
|
2
|
+
# use `rye lock` or `rye sync` to update this lockfile
|
3
|
+
#
|
4
|
+
# last locked with the following flags:
|
5
|
+
# pre: false
|
6
|
+
# features: []
|
7
|
+
# all-features: false
|
8
|
+
# with-sources: false
|
9
|
+
# generate-hashes: false
|
10
|
+
# universal: false
|
11
|
+
|
12
|
+
-e file:.
|
13
|
+
aioboto3==15.0.0
|
14
|
+
# via nonebot-plugin-awsmgmt
|
15
|
+
aiobotocore==2.23.0
|
16
|
+
# via aioboto3
|
17
|
+
aiofiles==24.1.0
|
18
|
+
# via aioboto3
|
19
|
+
aiohappyeyeballs==2.6.1
|
20
|
+
# via aiohttp
|
21
|
+
aiohttp==3.12.13
|
22
|
+
# via aiobotocore
|
23
|
+
aioitertools==0.12.0
|
24
|
+
# via aiobotocore
|
25
|
+
aiosignal==1.3.2
|
26
|
+
# via aiohttp
|
27
|
+
annotated-types==0.7.0
|
28
|
+
# via pydantic
|
29
|
+
anyio==4.9.0
|
30
|
+
# via nonebot2
|
31
|
+
# via starlette
|
32
|
+
# via watchfiles
|
33
|
+
async-timeout==5.0.1
|
34
|
+
# via aiohttp
|
35
|
+
attrs==25.3.0
|
36
|
+
# via aiohttp
|
37
|
+
boto3==1.38.27
|
38
|
+
# via aiobotocore
|
39
|
+
botocore==1.38.27
|
40
|
+
# via aiobotocore
|
41
|
+
# via boto3
|
42
|
+
# via s3transfer
|
43
|
+
click==8.2.1
|
44
|
+
# via uvicorn
|
45
|
+
exceptiongroup==1.3.0
|
46
|
+
# via anyio
|
47
|
+
# via nonebot2
|
48
|
+
fastapi==0.115.14
|
49
|
+
# via nonebot2
|
50
|
+
frozenlist==1.7.0
|
51
|
+
# via aiohttp
|
52
|
+
# via aiosignal
|
53
|
+
h11==0.16.0
|
54
|
+
# via uvicorn
|
55
|
+
httptools==0.6.4
|
56
|
+
# via uvicorn
|
57
|
+
idna==3.10
|
58
|
+
# via anyio
|
59
|
+
# via yarl
|
60
|
+
jmespath==1.0.1
|
61
|
+
# via aiobotocore
|
62
|
+
# via boto3
|
63
|
+
# via botocore
|
64
|
+
loguru==0.7.3
|
65
|
+
# via nonebot2
|
66
|
+
multidict==6.6.2
|
67
|
+
# via aiobotocore
|
68
|
+
# via aiohttp
|
69
|
+
# via yarl
|
70
|
+
nonebot2==2.4.2
|
71
|
+
# via nonebot-plugin-awsmgmt
|
72
|
+
propcache==0.3.2
|
73
|
+
# via aiohttp
|
74
|
+
# via yarl
|
75
|
+
pydantic==2.11.7
|
76
|
+
# via fastapi
|
77
|
+
# via nonebot2
|
78
|
+
pydantic-core==2.33.2
|
79
|
+
# via pydantic
|
80
|
+
pygtrie==2.5.0
|
81
|
+
# via nonebot2
|
82
|
+
python-dateutil==2.9.0.post0
|
83
|
+
# via aiobotocore
|
84
|
+
# via botocore
|
85
|
+
python-dotenv==1.1.1
|
86
|
+
# via nonebot2
|
87
|
+
# via uvicorn
|
88
|
+
pyyaml==6.0.2
|
89
|
+
# via uvicorn
|
90
|
+
s3transfer==0.13.0
|
91
|
+
# via boto3
|
92
|
+
six==1.17.0
|
93
|
+
# via python-dateutil
|
94
|
+
sniffio==1.3.1
|
95
|
+
# via anyio
|
96
|
+
starlette==0.46.2
|
97
|
+
# via fastapi
|
98
|
+
tomli==2.2.1
|
99
|
+
# via nonebot2
|
100
|
+
typing-extensions==4.14.0
|
101
|
+
# via anyio
|
102
|
+
# via exceptiongroup
|
103
|
+
# via fastapi
|
104
|
+
# via multidict
|
105
|
+
# via nonebot2
|
106
|
+
# via pydantic
|
107
|
+
# via pydantic-core
|
108
|
+
# via typing-inspection
|
109
|
+
# via uvicorn
|
110
|
+
typing-inspection==0.4.1
|
111
|
+
# via pydantic
|
112
|
+
urllib3==2.5.0
|
113
|
+
# via botocore
|
114
|
+
uvicorn==0.35.0
|
115
|
+
# via nonebot2
|
116
|
+
uvloop==0.21.0
|
117
|
+
# via uvicorn
|
118
|
+
watchfiles==1.1.0
|
119
|
+
# via uvicorn
|
120
|
+
websockets==15.0.1
|
121
|
+
# via uvicorn
|
122
|
+
wrapt==1.17.2
|
123
|
+
# via aiobotocore
|
124
|
+
yarl==1.20.1
|
125
|
+
# via aiohttp
|
126
|
+
# via nonebot2
|