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.
@@ -0,0 +1,10 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
@@ -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
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from nonebot-plugin-awsmgmt!"