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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -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)