django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
mojo/helpers/aws/ec2.py
ADDED
@@ -0,0 +1,804 @@
|
|
1
|
+
"""
|
2
|
+
AWS EC2 Helper Module
|
3
|
+
|
4
|
+
Provides simple interfaces for managing AWS EC2 (Elastic Compute Cloud) resources.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import time
|
9
|
+
import boto3
|
10
|
+
import botocore
|
11
|
+
from typing import Dict, List, Optional, Union, Any, Tuple
|
12
|
+
|
13
|
+
from .client import get_session
|
14
|
+
from mojo.helpers.settings import settings
|
15
|
+
from mojo.helpers import logit
|
16
|
+
|
17
|
+
logger = logit.get_logger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class EC2Instance:
|
21
|
+
"""
|
22
|
+
Simple interface for EC2 instance management.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, instance_id: Optional[str] = None, access_key: Optional[str] = None,
|
26
|
+
secret_key: Optional[str] = None, region: Optional[str] = None):
|
27
|
+
"""
|
28
|
+
Initialize an EC2 instance manager.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
instance_id: Optional EC2 instance ID
|
32
|
+
access_key: AWS access key, defaults to settings.AWS_KEY
|
33
|
+
secret_key: AWS secret key, defaults to settings.AWS_SECRET
|
34
|
+
region: AWS region, defaults to settings.AWS_REGION if available
|
35
|
+
"""
|
36
|
+
self.instance_id = instance_id
|
37
|
+
self.access_key = access_key or settings.AWS_KEY
|
38
|
+
self.secret_key = secret_key or settings.AWS_SECRET
|
39
|
+
self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
|
40
|
+
|
41
|
+
session = get_session(self.access_key, self.secret_key, self.region)
|
42
|
+
self.client = session.client('ec2')
|
43
|
+
self.resource = session.resource('ec2')
|
44
|
+
|
45
|
+
self.instance = None
|
46
|
+
if instance_id:
|
47
|
+
self.instance = self.resource.Instance(instance_id)
|
48
|
+
self.exists = self._check_exists()
|
49
|
+
|
50
|
+
def _check_exists(self) -> bool:
|
51
|
+
"""Check if the instance exists."""
|
52
|
+
try:
|
53
|
+
self.instance.load()
|
54
|
+
# Check if the instance state is not 'terminated'
|
55
|
+
return self.instance.state['Name'] != 'terminated'
|
56
|
+
except botocore.exceptions.ClientError as e:
|
57
|
+
if e.response['Error']['Code'] == 'InvalidInstanceID.NotFound':
|
58
|
+
return False
|
59
|
+
logger.error(f"Error checking instance existence: {e}")
|
60
|
+
raise
|
61
|
+
|
62
|
+
def create(self,
|
63
|
+
ami_id: str,
|
64
|
+
instance_type: str = 't2.micro',
|
65
|
+
key_name: Optional[str] = None,
|
66
|
+
security_group_ids: Optional[List[str]] = None,
|
67
|
+
subnet_id: Optional[str] = None,
|
68
|
+
user_data: Optional[str] = None,
|
69
|
+
tags: Optional[List[Dict[str, str]]] = None,
|
70
|
+
count: int = 1,
|
71
|
+
wait_until_running: bool = True) -> Dict:
|
72
|
+
"""
|
73
|
+
Create a new EC2 instance.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
ami_id: Amazon Machine Image ID
|
77
|
+
instance_type: EC2 instance type (e.g. t2.micro)
|
78
|
+
key_name: SSH key pair name
|
79
|
+
security_group_ids: List of security group IDs
|
80
|
+
subnet_id: VPC subnet ID
|
81
|
+
user_data: Initialization script
|
82
|
+
tags: List of tags for the instance
|
83
|
+
count: Number of instances to launch
|
84
|
+
wait_until_running: Whether to wait until the instance is running
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Dict containing instance information
|
88
|
+
"""
|
89
|
+
try:
|
90
|
+
# Prepare run parameters
|
91
|
+
run_params = {
|
92
|
+
'ImageId': ami_id,
|
93
|
+
'InstanceType': instance_type,
|
94
|
+
'MinCount': count,
|
95
|
+
'MaxCount': count
|
96
|
+
}
|
97
|
+
|
98
|
+
if key_name:
|
99
|
+
run_params['KeyName'] = key_name
|
100
|
+
|
101
|
+
if security_group_ids:
|
102
|
+
run_params['SecurityGroupIds'] = security_group_ids
|
103
|
+
|
104
|
+
if subnet_id:
|
105
|
+
run_params['SubnetId'] = subnet_id
|
106
|
+
|
107
|
+
if user_data:
|
108
|
+
run_params['UserData'] = user_data
|
109
|
+
|
110
|
+
# Launch the instance
|
111
|
+
response = self.client.run_instances(**run_params)
|
112
|
+
instances = response['Instances']
|
113
|
+
|
114
|
+
# Add tags if provided
|
115
|
+
if tags and instances:
|
116
|
+
instance_ids = [instance['InstanceId'] for instance in instances]
|
117
|
+
self.client.create_tags(
|
118
|
+
Resources=instance_ids,
|
119
|
+
Tags=tags
|
120
|
+
)
|
121
|
+
|
122
|
+
# Wait until the instance is running if requested
|
123
|
+
if wait_until_running and instances:
|
124
|
+
instance_ids = [instance['InstanceId'] for instance in instances]
|
125
|
+
waiter = self.client.get_waiter('instance_running')
|
126
|
+
waiter.wait(InstanceIds=instance_ids)
|
127
|
+
|
128
|
+
# Reload instances to get the latest state
|
129
|
+
instances = []
|
130
|
+
for instance_id in instance_ids:
|
131
|
+
instance = self.resource.Instance(instance_id)
|
132
|
+
instance.load()
|
133
|
+
instances.append({
|
134
|
+
'InstanceId': instance.id,
|
135
|
+
'PublicIpAddress': instance.public_ip_address,
|
136
|
+
'PrivateIpAddress': instance.private_ip_address,
|
137
|
+
'State': instance.state['Name']
|
138
|
+
})
|
139
|
+
|
140
|
+
# If only one instance was created, set it as the current instance
|
141
|
+
if count == 1 and instances:
|
142
|
+
self.instance_id = instances[0]['InstanceId']
|
143
|
+
self.instance = self.resource.Instance(self.instance_id)
|
144
|
+
self.exists = True
|
145
|
+
|
146
|
+
return {'Instances': instances}
|
147
|
+
except botocore.exceptions.ClientError as e:
|
148
|
+
logger.error(f"Failed to create EC2 instance: {e}")
|
149
|
+
return {'Error': str(e)}
|
150
|
+
|
151
|
+
def terminate(self, wait_until_terminated: bool = True) -> bool:
|
152
|
+
"""
|
153
|
+
Terminate the EC2 instance.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
wait_until_terminated: Whether to wait until the instance is terminated
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
True if successfully terminated, False otherwise
|
160
|
+
"""
|
161
|
+
if not self.instance_id or not self.exists:
|
162
|
+
logger.warning("No valid instance to terminate")
|
163
|
+
return False
|
164
|
+
|
165
|
+
try:
|
166
|
+
self.instance.terminate()
|
167
|
+
|
168
|
+
if wait_until_terminated:
|
169
|
+
waiter = self.client.get_waiter('instance_terminated')
|
170
|
+
waiter.wait(InstanceIds=[self.instance_id])
|
171
|
+
|
172
|
+
self.exists = False
|
173
|
+
return True
|
174
|
+
except botocore.exceptions.ClientError as e:
|
175
|
+
logger.error(f"Failed to terminate instance {self.instance_id}: {e}")
|
176
|
+
return False
|
177
|
+
|
178
|
+
def start(self, wait_until_running: bool = True) -> bool:
|
179
|
+
"""
|
180
|
+
Start the EC2 instance.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
wait_until_running: Whether to wait until the instance is running
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
True if successfully started, False otherwise
|
187
|
+
"""
|
188
|
+
if not self.instance_id or not self.exists:
|
189
|
+
logger.warning("No valid instance to start")
|
190
|
+
return False
|
191
|
+
|
192
|
+
try:
|
193
|
+
# Only start if the instance is stopped
|
194
|
+
if self.instance.state['Name'] == 'stopped':
|
195
|
+
self.instance.start()
|
196
|
+
|
197
|
+
if wait_until_running:
|
198
|
+
waiter = self.client.get_waiter('instance_running')
|
199
|
+
waiter.wait(InstanceIds=[self.instance_id])
|
200
|
+
self.instance.load() # Reload to get the latest state
|
201
|
+
|
202
|
+
return True
|
203
|
+
else:
|
204
|
+
logger.info(f"Instance {self.instance_id} is not in 'stopped' state (current: {self.instance.state['Name']})")
|
205
|
+
return False
|
206
|
+
except botocore.exceptions.ClientError as e:
|
207
|
+
logger.error(f"Failed to start instance {self.instance_id}: {e}")
|
208
|
+
return False
|
209
|
+
|
210
|
+
def stop(self, wait_until_stopped: bool = True) -> bool:
|
211
|
+
"""
|
212
|
+
Stop the EC2 instance.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
wait_until_stopped: Whether to wait until the instance is stopped
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
True if successfully stopped, False otherwise
|
219
|
+
"""
|
220
|
+
if not self.instance_id or not self.exists:
|
221
|
+
logger.warning("No valid instance to stop")
|
222
|
+
return False
|
223
|
+
|
224
|
+
try:
|
225
|
+
# Only stop if the instance is running
|
226
|
+
if self.instance.state['Name'] == 'running':
|
227
|
+
self.instance.stop()
|
228
|
+
|
229
|
+
if wait_until_stopped:
|
230
|
+
waiter = self.client.get_waiter('instance_stopped')
|
231
|
+
waiter.wait(InstanceIds=[self.instance_id])
|
232
|
+
self.instance.load() # Reload to get the latest state
|
233
|
+
|
234
|
+
return True
|
235
|
+
else:
|
236
|
+
logger.info(f"Instance {self.instance_id} is not in 'running' state (current: {self.instance.state['Name']})")
|
237
|
+
return False
|
238
|
+
except botocore.exceptions.ClientError as e:
|
239
|
+
logger.error(f"Failed to stop instance {self.instance_id}: {e}")
|
240
|
+
return False
|
241
|
+
|
242
|
+
def reboot(self) -> bool:
|
243
|
+
"""
|
244
|
+
Reboot the EC2 instance.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
True if reboot initiated successfully, False otherwise
|
248
|
+
"""
|
249
|
+
if not self.instance_id or not self.exists:
|
250
|
+
logger.warning("No valid instance to reboot")
|
251
|
+
return False
|
252
|
+
|
253
|
+
try:
|
254
|
+
self.instance.reboot()
|
255
|
+
return True
|
256
|
+
except botocore.exceptions.ClientError as e:
|
257
|
+
logger.error(f"Failed to reboot instance {self.instance_id}: {e}")
|
258
|
+
return False
|
259
|
+
|
260
|
+
def get_status(self) -> Dict:
|
261
|
+
"""
|
262
|
+
Get the current status of the instance.
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
Dict containing instance status information
|
266
|
+
"""
|
267
|
+
if not self.instance_id or not self.exists:
|
268
|
+
logger.warning("No valid instance to get status for")
|
269
|
+
return {}
|
270
|
+
|
271
|
+
try:
|
272
|
+
self.instance.load()
|
273
|
+
return {
|
274
|
+
'InstanceId': self.instance.id,
|
275
|
+
'State': self.instance.state['Name'],
|
276
|
+
'InstanceType': self.instance.instance_type,
|
277
|
+
'PublicIpAddress': self.instance.public_ip_address,
|
278
|
+
'PrivateIpAddress': self.instance.private_ip_address,
|
279
|
+
'LaunchTime': self.instance.launch_time.isoformat() if hasattr(self.instance, 'launch_time') else None,
|
280
|
+
'Tags': self.instance.tags
|
281
|
+
}
|
282
|
+
except botocore.exceptions.ClientError as e:
|
283
|
+
logger.error(f"Failed to get status for instance {self.instance_id}: {e}")
|
284
|
+
return {}
|
285
|
+
|
286
|
+
def add_tags(self, tags: List[Dict[str, str]]) -> bool:
|
287
|
+
"""
|
288
|
+
Add tags to the instance.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
tags: List of tags to add
|
292
|
+
|
293
|
+
Returns:
|
294
|
+
True if successful, False otherwise
|
295
|
+
"""
|
296
|
+
if not self.instance_id or not self.exists:
|
297
|
+
logger.warning("No valid instance to add tags to")
|
298
|
+
return False
|
299
|
+
|
300
|
+
try:
|
301
|
+
self.instance.create_tags(Tags=tags)
|
302
|
+
return True
|
303
|
+
except botocore.exceptions.ClientError as e:
|
304
|
+
logger.error(f"Failed to add tags to instance {self.instance_id}: {e}")
|
305
|
+
return False
|
306
|
+
|
307
|
+
def get_console_output(self) -> str:
|
308
|
+
"""
|
309
|
+
Get the console output of the instance.
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
Console output as a string
|
313
|
+
"""
|
314
|
+
if not self.instance_id or not self.exists:
|
315
|
+
logger.warning("No valid instance to get console output for")
|
316
|
+
return ""
|
317
|
+
|
318
|
+
try:
|
319
|
+
response = self.client.get_console_output(InstanceId=self.instance_id)
|
320
|
+
return response.get('Output', '')
|
321
|
+
except botocore.exceptions.ClientError as e:
|
322
|
+
logger.error(f"Failed to get console output for instance {self.instance_id}: {e}")
|
323
|
+
return ""
|
324
|
+
|
325
|
+
@staticmethod
|
326
|
+
def list_instances(filters: Optional[List[Dict[str, Any]]] = None) -> List[Dict]:
|
327
|
+
"""
|
328
|
+
List EC2 instances with optional filtering.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
filters: Optional list of filters
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
List of instance dictionaries
|
335
|
+
"""
|
336
|
+
client = boto3.client('ec2',
|
337
|
+
aws_access_key_id=settings.AWS_KEY,
|
338
|
+
aws_secret_access_key=settings.AWS_SECRET,
|
339
|
+
region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
|
340
|
+
|
341
|
+
try:
|
342
|
+
if filters:
|
343
|
+
response = client.describe_instances(Filters=filters)
|
344
|
+
else:
|
345
|
+
response = client.describe_instances()
|
346
|
+
|
347
|
+
instances = []
|
348
|
+
for reservation in response.get('Reservations', []):
|
349
|
+
for instance in reservation.get('Instances', []):
|
350
|
+
instances.append(instance)
|
351
|
+
|
352
|
+
return instances
|
353
|
+
except botocore.exceptions.ClientError as e:
|
354
|
+
logger.error(f"Failed to list instances: {e}")
|
355
|
+
return []
|
356
|
+
|
357
|
+
@staticmethod
|
358
|
+
def get_instance_by_tag(tag_key: str, tag_value: str) -> Optional[str]:
|
359
|
+
"""
|
360
|
+
Find an instance by tag.
|
361
|
+
|
362
|
+
Args:
|
363
|
+
tag_key: Tag key to search for
|
364
|
+
tag_value: Tag value to match
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
Instance ID if found, None otherwise
|
368
|
+
"""
|
369
|
+
filters = [
|
370
|
+
{
|
371
|
+
'Name': f'tag:{tag_key}',
|
372
|
+
'Values': [tag_value]
|
373
|
+
}
|
374
|
+
]
|
375
|
+
|
376
|
+
instances = EC2Instance.list_instances(filters)
|
377
|
+
if instances:
|
378
|
+
return instances[0]['InstanceId']
|
379
|
+
return None
|
380
|
+
|
381
|
+
|
382
|
+
class EC2SecurityGroup:
|
383
|
+
"""
|
384
|
+
Simple interface for EC2 security group management.
|
385
|
+
"""
|
386
|
+
|
387
|
+
def __init__(self, group_id: Optional[str] = None, access_key: Optional[str] = None,
|
388
|
+
secret_key: Optional[str] = None, region: Optional[str] = None):
|
389
|
+
"""
|
390
|
+
Initialize a security group manager.
|
391
|
+
|
392
|
+
Args:
|
393
|
+
group_id: Optional security group ID
|
394
|
+
access_key: AWS access key, defaults to settings.AWS_KEY
|
395
|
+
secret_key: AWS secret key, defaults to settings.AWS_SECRET
|
396
|
+
region: AWS region, defaults to settings.AWS_REGION if available
|
397
|
+
"""
|
398
|
+
self.group_id = group_id
|
399
|
+
self.access_key = access_key or settings.AWS_KEY
|
400
|
+
self.secret_key = secret_key or settings.AWS_SECRET
|
401
|
+
self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
|
402
|
+
|
403
|
+
session = get_session(self.access_key, self.secret_key, self.region)
|
404
|
+
self.client = session.client('ec2')
|
405
|
+
self.resource = session.resource('ec2')
|
406
|
+
|
407
|
+
self.security_group = None
|
408
|
+
if group_id:
|
409
|
+
self.security_group = self.resource.SecurityGroup(group_id)
|
410
|
+
self.exists = self._check_exists()
|
411
|
+
|
412
|
+
def _check_exists(self) -> bool:
|
413
|
+
"""Check if the security group exists."""
|
414
|
+
try:
|
415
|
+
self.security_group.load()
|
416
|
+
return True
|
417
|
+
except botocore.exceptions.ClientError as e:
|
418
|
+
if e.response['Error']['Code'] == 'InvalidGroup.NotFound':
|
419
|
+
return False
|
420
|
+
logger.error(f"Error checking security group existence: {e}")
|
421
|
+
raise
|
422
|
+
|
423
|
+
def create(self, name: str, description: str, vpc_id: Optional[str] = None,
|
424
|
+
tags: Optional[List[Dict[str, str]]] = None) -> bool:
|
425
|
+
"""
|
426
|
+
Create a new security group.
|
427
|
+
|
428
|
+
Args:
|
429
|
+
name: Security group name
|
430
|
+
description: Security group description
|
431
|
+
vpc_id: Optional VPC ID
|
432
|
+
tags: Optional tags for the security group
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
True if successful, False otherwise
|
436
|
+
"""
|
437
|
+
try:
|
438
|
+
# Prepare creation parameters
|
439
|
+
create_params = {
|
440
|
+
'GroupName': name,
|
441
|
+
'Description': description
|
442
|
+
}
|
443
|
+
|
444
|
+
if vpc_id:
|
445
|
+
create_params['VpcId'] = vpc_id
|
446
|
+
|
447
|
+
# Create the security group
|
448
|
+
response = self.client.create_security_group(**create_params)
|
449
|
+
self.group_id = response['GroupId']
|
450
|
+
self.security_group = self.resource.SecurityGroup(self.group_id)
|
451
|
+
self.exists = True
|
452
|
+
|
453
|
+
# Add tags if provided
|
454
|
+
if tags:
|
455
|
+
self.security_group.create_tags(Tags=tags)
|
456
|
+
|
457
|
+
return True
|
458
|
+
except botocore.exceptions.ClientError as e:
|
459
|
+
logger.error(f"Failed to create security group: {e}")
|
460
|
+
return False
|
461
|
+
|
462
|
+
def delete(self) -> bool:
|
463
|
+
"""
|
464
|
+
Delete the security group.
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
True if successful, False otherwise
|
468
|
+
"""
|
469
|
+
if not self.group_id or not self.exists:
|
470
|
+
logger.warning("No valid security group to delete")
|
471
|
+
return False
|
472
|
+
|
473
|
+
try:
|
474
|
+
self.security_group.delete()
|
475
|
+
self.exists = False
|
476
|
+
return True
|
477
|
+
except botocore.exceptions.ClientError as e:
|
478
|
+
logger.error(f"Failed to delete security group {self.group_id}: {e}")
|
479
|
+
return False
|
480
|
+
|
481
|
+
def authorize_ingress(self, ip_protocol: str, from_port: int, to_port: int,
|
482
|
+
cidr_ip: Optional[str] = None,
|
483
|
+
source_group_id: Optional[str] = None,
|
484
|
+
description: Optional[str] = None) -> bool:
|
485
|
+
"""
|
486
|
+
Add an inbound rule to the security group.
|
487
|
+
|
488
|
+
Args:
|
489
|
+
ip_protocol: IP protocol (tcp, udp, icmp)
|
490
|
+
from_port: Start port
|
491
|
+
to_port: End port
|
492
|
+
cidr_ip: CIDR IP range
|
493
|
+
source_group_id: Source security group ID
|
494
|
+
description: Rule description
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
True if successful, False otherwise
|
498
|
+
"""
|
499
|
+
if not self.group_id or not self.exists:
|
500
|
+
logger.warning("No valid security group to add rule to")
|
501
|
+
return False
|
502
|
+
|
503
|
+
try:
|
504
|
+
rule_params = {
|
505
|
+
'IpProtocol': ip_protocol,
|
506
|
+
'FromPort': from_port,
|
507
|
+
'ToPort': to_port,
|
508
|
+
}
|
509
|
+
|
510
|
+
if cidr_ip:
|
511
|
+
rule_params['CidrIp'] = cidr_ip
|
512
|
+
elif source_group_id:
|
513
|
+
rule_params['SourceSecurityGroupId'] = source_group_id
|
514
|
+
else:
|
515
|
+
raise ValueError("Either cidr_ip or source_group_id must be provided")
|
516
|
+
|
517
|
+
if description:
|
518
|
+
rule_params['Description'] = description
|
519
|
+
|
520
|
+
self.security_group.authorize_ingress(
|
521
|
+
GroupId=self.group_id,
|
522
|
+
IpPermissions=[rule_params]
|
523
|
+
)
|
524
|
+
return True
|
525
|
+
except botocore.exceptions.ClientError as e:
|
526
|
+
if 'InvalidPermission.Duplicate' in str(e):
|
527
|
+
# Rule already exists, not a failure
|
528
|
+
logger.info(f"Rule already exists in security group {self.group_id}")
|
529
|
+
return True
|
530
|
+
logger.error(f"Failed to add ingress rule to security group {self.group_id}: {e}")
|
531
|
+
return False
|
532
|
+
|
533
|
+
def authorize_egress(self, ip_protocol: str, from_port: int, to_port: int,
|
534
|
+
cidr_ip: Optional[str] = None,
|
535
|
+
destination_group_id: Optional[str] = None,
|
536
|
+
description: Optional[str] = None) -> bool:
|
537
|
+
"""
|
538
|
+
Add an outbound rule to the security group.
|
539
|
+
|
540
|
+
Args:
|
541
|
+
ip_protocol: IP protocol (tcp, udp, icmp)
|
542
|
+
from_port: Start port
|
543
|
+
to_port: End port
|
544
|
+
cidr_ip: CIDR IP range
|
545
|
+
destination_group_id: Destination security group ID
|
546
|
+
description: Rule description
|
547
|
+
|
548
|
+
Returns:
|
549
|
+
True if successful, False otherwise
|
550
|
+
"""
|
551
|
+
if not self.group_id or not self.exists:
|
552
|
+
logger.warning("No valid security group to add rule to")
|
553
|
+
return False
|
554
|
+
|
555
|
+
try:
|
556
|
+
rule_params = {
|
557
|
+
'IpProtocol': ip_protocol,
|
558
|
+
'FromPort': from_port,
|
559
|
+
'ToPort': to_port,
|
560
|
+
}
|
561
|
+
|
562
|
+
if cidr_ip:
|
563
|
+
rule_params['CidrIp'] = cidr_ip
|
564
|
+
elif destination_group_id:
|
565
|
+
rule_params['DestinationSecurityGroupId'] = destination_group_id
|
566
|
+
else:
|
567
|
+
raise ValueError("Either cidr_ip or destination_group_id must be provided")
|
568
|
+
|
569
|
+
if description:
|
570
|
+
rule_params['Description'] = description
|
571
|
+
|
572
|
+
self.security_group.authorize_egress(
|
573
|
+
GroupId=self.group_id,
|
574
|
+
IpPermissions=[rule_params]
|
575
|
+
)
|
576
|
+
return True
|
577
|
+
except botocore.exceptions.ClientError as e:
|
578
|
+
if 'InvalidPermission.Duplicate' in str(e):
|
579
|
+
# Rule already exists, not a failure
|
580
|
+
logger.info(f"Rule already exists in security group {self.group_id}")
|
581
|
+
return True
|
582
|
+
logger.error(f"Failed to add egress rule to security group {self.group_id}: {e}")
|
583
|
+
return False
|
584
|
+
|
585
|
+
def revoke_ingress(self, ip_protocol: str, from_port: int, to_port: int,
|
586
|
+
cidr_ip: Optional[str] = None,
|
587
|
+
source_group_id: Optional[str] = None) -> bool:
|
588
|
+
"""
|
589
|
+
Remove an inbound rule from the security group.
|
590
|
+
|
591
|
+
Args:
|
592
|
+
ip_protocol: IP protocol (tcp, udp, icmp)
|
593
|
+
from_port: Start port
|
594
|
+
to_port: End port
|
595
|
+
cidr_ip: CIDR IP range
|
596
|
+
source_group_id: Source security group ID
|
597
|
+
|
598
|
+
Returns:
|
599
|
+
True if successful, False otherwise
|
600
|
+
"""
|
601
|
+
if not self.group_id or not self.exists:
|
602
|
+
logger.warning("No valid security group to remove rule from")
|
603
|
+
return False
|
604
|
+
|
605
|
+
try:
|
606
|
+
rule_params = {
|
607
|
+
'IpProtocol': ip_protocol,
|
608
|
+
'FromPort': from_port,
|
609
|
+
'ToPort': to_port,
|
610
|
+
}
|
611
|
+
|
612
|
+
if cidr_ip:
|
613
|
+
rule_params['CidrIp'] = cidr_ip
|
614
|
+
elif source_group_id:
|
615
|
+
rule_params['SourceSecurityGroupId'] = source_group_id
|
616
|
+
else:
|
617
|
+
raise ValueError("Either cidr_ip or source_group_id must be provided")
|
618
|
+
|
619
|
+
self.security_group.revoke_ingress(
|
620
|
+
GroupId=self.group_id,
|
621
|
+
IpPermissions=[rule_params]
|
622
|
+
)
|
623
|
+
return True
|
624
|
+
except botocore.exceptions.ClientError as e:
|
625
|
+
logger.error(f"Failed to remove ingress rule from security group {self.group_id}: {e}")
|
626
|
+
return False
|
627
|
+
|
628
|
+
def revoke_egress(self, ip_protocol: str, from_port: int, to_port: int,
|
629
|
+
cidr_ip: Optional[str] = None,
|
630
|
+
destination_group_id: Optional[str] = None) -> bool:
|
631
|
+
"""
|
632
|
+
Remove an outbound rule from the security group.
|
633
|
+
|
634
|
+
Args:
|
635
|
+
ip_protocol: IP protocol (tcp, udp, icmp)
|
636
|
+
from_port: Start port
|
637
|
+
to_port: End port
|
638
|
+
cidr_ip: CIDR IP range
|
639
|
+
destination_group_id: Destination security group ID
|
640
|
+
|
641
|
+
Returns:
|
642
|
+
True if successful, False otherwise
|
643
|
+
"""
|
644
|
+
if not self.group_id or not self.exists:
|
645
|
+
logger.warning("No valid security group to remove rule from")
|
646
|
+
return False
|
647
|
+
|
648
|
+
try:
|
649
|
+
rule_params = {
|
650
|
+
'IpProtocol': ip_protocol,
|
651
|
+
'FromPort': from_port,
|
652
|
+
'ToPort': to_port,
|
653
|
+
}
|
654
|
+
|
655
|
+
if cidr_ip:
|
656
|
+
rule_params['CidrIp'] = cidr_ip
|
657
|
+
elif destination_group_id:
|
658
|
+
rule_params['DestinationSecurityGroupId'] = destination_group_id
|
659
|
+
else:
|
660
|
+
raise ValueError("Either cidr_ip or destination_group_id must be provided")
|
661
|
+
|
662
|
+
self.security_group.revoke_egress(
|
663
|
+
GroupId=self.group_id,
|
664
|
+
IpPermissions=[rule_params]
|
665
|
+
)
|
666
|
+
return True
|
667
|
+
except botocore.exceptions.ClientError as e:
|
668
|
+
logger.error(f"Failed to remove egress rule from security group {self.group_id}: {e}")
|
669
|
+
return False
|
670
|
+
|
671
|
+
def get_rules(self) -> Dict[str, List]:
|
672
|
+
"""
|
673
|
+
Get all rules for the security group.
|
674
|
+
|
675
|
+
Returns:
|
676
|
+
Dict with 'Ingress' and 'Egress' rule lists
|
677
|
+
"""
|
678
|
+
if not self.group_id or not self.exists:
|
679
|
+
logger.warning("No valid security group to get rules for")
|
680
|
+
return {'Ingress': [], 'Egress': []}
|
681
|
+
|
682
|
+
try:
|
683
|
+
self.security_group.load()
|
684
|
+
return {
|
685
|
+
'Ingress': self.security_group.ip_permissions,
|
686
|
+
'Egress': self.security_group.ip_permissions_egress
|
687
|
+
}
|
688
|
+
except botocore.exceptions.ClientError as e:
|
689
|
+
logger.error(f"Failed to get rules for security group {self.group_id}: {e}")
|
690
|
+
return {'Ingress': [], 'Egress': []}
|
691
|
+
|
692
|
+
@staticmethod
|
693
|
+
def list_security_groups(filters: Optional[List[Dict[str, Any]]] = None) -> List[Dict]:
|
694
|
+
"""
|
695
|
+
List security groups with optional filtering.
|
696
|
+
|
697
|
+
Args:
|
698
|
+
filters: Optional list of filters
|
699
|
+
|
700
|
+
Returns:
|
701
|
+
List of security group dictionaries
|
702
|
+
"""
|
703
|
+
client = boto3.client('ec2',
|
704
|
+
aws_access_key_id=settings.AWS_KEY,
|
705
|
+
aws_secret_access_key=settings.AWS_SECRET,
|
706
|
+
region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
|
707
|
+
|
708
|
+
try:
|
709
|
+
if filters:
|
710
|
+
response = client.describe_security_groups(Filters=filters)
|
711
|
+
else:
|
712
|
+
response = client.describe_security_groups()
|
713
|
+
|
714
|
+
return response.get('SecurityGroups', [])
|
715
|
+
except botocore.exceptions.ClientError as e:
|
716
|
+
logger.error(f"Failed to list security groups: {e}")
|
717
|
+
return []
|
718
|
+
|
719
|
+
|
720
|
+
# Utility functions
|
721
|
+
def create_web_server_security_group(name: str, description: str = "Web server security group",
|
722
|
+
vpc_id: Optional[str] = None) -> Optional[str]:
|
723
|
+
"""
|
724
|
+
Create a security group with common web server rules (HTTP, HTTPS, SSH).
|
725
|
+
|
726
|
+
Args:
|
727
|
+
name: Security group name
|
728
|
+
description: Security group description
|
729
|
+
vpc_id: Optional VPC ID
|
730
|
+
|
731
|
+
Returns:
|
732
|
+
Security group ID if successful, None otherwise
|
733
|
+
"""
|
734
|
+
sg = EC2SecurityGroup()
|
735
|
+
|
736
|
+
if not sg.create(name, description, vpc_id):
|
737
|
+
return None
|
738
|
+
|
739
|
+
# Add common inbound rules
|
740
|
+
sg.authorize_ingress('tcp', 80, 80, '0.0.0.0/0', description="HTTP")
|
741
|
+
sg.authorize_ingress('tcp', 443, 443, '0.0.0.0/0', description="HTTPS")
|
742
|
+
sg.authorize_ingress('tcp', 22, 22, '0.0.0.0/0', description="SSH")
|
743
|
+
|
744
|
+
return sg.group_id
|
745
|
+
|
746
|
+
|
747
|
+
def launch_instance(ami_id: str, instance_type: str = 't2.micro',
|
748
|
+
key_name: Optional[str] = None,
|
749
|
+
security_group_ids: Optional[List[str]] = None,
|
750
|
+
name_tag: Optional[str] = None,
|
751
|
+
user_data: Optional[str] = None) -> Dict:
|
752
|
+
"""
|
753
|
+
Launch an EC2 instance with common defaults.
|
754
|
+
|
755
|
+
Args:
|
756
|
+
ami_id: Amazon Machine Image ID
|
757
|
+
instance_type: EC2 instance type
|
758
|
+
key_name: SSH key pair name
|
759
|
+
security_group_ids: List of security group IDs
|
760
|
+
name_tag: Name tag for the instance
|
761
|
+
user_data: Initialization script
|
762
|
+
|
763
|
+
Returns:
|
764
|
+
Dict with instance information
|
765
|
+
"""
|
766
|
+
instance = EC2Instance()
|
767
|
+
|
768
|
+
# Prepare tags if a name was provided
|
769
|
+
tags = None
|
770
|
+
if name_tag:
|
771
|
+
tags = [{'Key': 'Name', 'Value': name_tag}]
|
772
|
+
|
773
|
+
# Launch the instance
|
774
|
+
result = instance.create(
|
775
|
+
ami_id=ami_id,
|
776
|
+
instance_type=instance_type,
|
777
|
+
key_name=key_name,
|
778
|
+
security_group_ids=security_group_ids,
|
779
|
+
user_data=user_data,
|
780
|
+
tags=tags,
|
781
|
+
wait_until_running=True
|
782
|
+
)
|
783
|
+
|
784
|
+
return result
|
785
|
+
|
786
|
+
|
787
|
+
def get_instances_by_state(state: str = 'running') -> List[Dict]:
|
788
|
+
"""
|
789
|
+
Get instances filtered by state.
|
790
|
+
|
791
|
+
Args:
|
792
|
+
state: Instance state (e.g., 'running', 'stopped')
|
793
|
+
|
794
|
+
Returns:
|
795
|
+
List of instance dictionaries
|
796
|
+
"""
|
797
|
+
filters = [
|
798
|
+
{
|
799
|
+
'Name': 'instance-state-name',
|
800
|
+
'Values': [state]
|
801
|
+
}
|
802
|
+
]
|
803
|
+
|
804
|
+
return EC2Instance.list_instances(filters)
|