Skip to content

Commit d4dae76

Browse files
committed
✨ [automated-actions] rds-logs action
1 parent 445629e commit d4dae76

File tree

20 files changed

+1537
-88
lines changed

20 files changed

+1537
-88
lines changed

actions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ These are the core operations that users can request the system to perform.
1212
* **Required Parameters**: The AWS account name and the ElastiCache cluster identifier.
1313
* **Usage Example (CLI)**: `automated-actions external-resource-flush-elasticache --account aws-account-name --identifier my-elasticache-cluster`
1414

15+
* **`external-resource-rds-logs`**:
16+
* **Description**: Retrieves logs from an Amazon RDS instance and stores them in an S3 bucket.
17+
* **Use Case**: Typically used for troubleshooting database issues, analyzing performance problems, or collecting logs for audit purposes.
18+
* **Required Parameters**: The AWS account name and the RDS instance identifier.
19+
* **Optional Parameters**: Expiration time in days (1-7, default: 7), S3 target file name (defaults to '{account}-{identifier}.zip').
20+
* **Usage Example (CLI)**: `automated-actions external-resource-rds-logs --account aws-account-name --identifier my-rds-instance --expiration-days 5 --s3-file-name my-custom-logs.zip`
21+
1522
* **`external-resource-rds-reboot`**:
1623
* **Description**: Reboots an Amazon RDS instance.
1724
* **Use Case**: Typically used for maintenance, applying updates, or resolving performance issues.

docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ services:
88
environment:
99
- DEBUG=${DEBUG-}
1010
- DOCKER_HOST=unix:///var/run/docker.sock
11-
- SERVICES=${SERVICES:-sqs,dynamodb}
11+
- SERVICES=${SERVICES:-sqs,dynamodb,s3}
1212
- PERSISTENCE=${PERSISTENCE:-0}
1313
volumes:
1414
- "${LOCALSTACK_VOLUME_DIR:-./.localstack_volume}:/var/lib/localstack"
@@ -83,6 +83,7 @@ services:
8383
- AA_BROKER_URL=sqs://localstack:4566
8484
- AA_SQS_URL=http://localstack:4566/000000000000/automated-actions
8585
- AA_DYNAMODB_URL=http://localstack:4566
86+
- AA_EXTERNAL_RESOURCE_RDS_LOGS__S3_URL=http://localstack:4566
8687
build:
8788
context: .
8889
dockerfile: Dockerfile

localstack/init-s3.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
3+
# create bucket
4+
awslocal s3api create-bucket --bucket automated-actions

packages/automated_actions/automated_actions/api/v1/views/external_resource.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,84 @@
77
from automated_actions.celery.external_resource.tasks import (
88
external_resource_flush_elasticache as external_resource_flush_elasticache_task,
99
)
10+
from automated_actions.celery.external_resource.tasks import (
11+
external_resource_rds_logs as external_resource_rds_logs_task,
12+
)
1013
from automated_actions.celery.external_resource.tasks import (
1114
external_resource_rds_reboot as external_resource_rds_reboot_task,
1215
)
1316
from automated_actions.celery.external_resource.tasks import (
1417
external_resource_rds_snapshot as external_resource_rds_snapshot_task,
1518
)
16-
from automated_actions.db.models import (
17-
Action,
18-
ActionSchemaOut,
19-
)
19+
from automated_actions.db.models import Action, ActionSchemaOut
2020
from automated_actions.db.models._action import ActionManager, get_action_manager
2121

2222
router = APIRouter()
2323
log = logging.getLogger(__name__)
2424

25-
EXTERNAL_RESOURCE_RDS_REBOOT_ACTION_ID = "external-resource-rds-reboot"
2625
EXTERNAL_RESOURCE_FLUSH_ELASTICACHE_ACTION_ID = "external-resource-flush-elasticache"
26+
EXTERNAL_RESOURCE_RDS_LOGS_ACTION_ID = "external-resource-rds-logs"
27+
EXTERNAL_RESOURCE_RDS_REBOOT_ACTION_ID = "external-resource-rds-reboot"
2728
EXTERNAL_RESOURCE_RDS_SNAPSHOT_ACTION_ID = "external-resource-rds-snapshot"
2829

2930

31+
def get_action_external_resource_rds_logs(
32+
action_mgr: Annotated[ActionManager, Depends(get_action_manager)], user: UserDep
33+
) -> Action:
34+
"""Get a new action object for the user.
35+
36+
Args:
37+
action_mgr: The action manager dependency.
38+
user: The user dependency.
39+
40+
Returns:
41+
A new Action object.
42+
"""
43+
return action_mgr.create_action(
44+
name=EXTERNAL_RESOURCE_RDS_LOGS_ACTION_ID, owner=user
45+
)
46+
47+
48+
@router.post(
49+
"/external-resource/rds-logs/{account}/{identifier}",
50+
operation_id=EXTERNAL_RESOURCE_RDS_LOGS_ACTION_ID,
51+
status_code=202,
52+
tags=["Actions"],
53+
)
54+
def external_resource_rds_logs(
55+
account: Annotated[str, Path(description="AWS account name")],
56+
identifier: Annotated[str, Path(description="RDS instance identifier")],
57+
action: Annotated[Action, Depends(get_action_external_resource_rds_logs)],
58+
expiration_days: Annotated[
59+
int, Query(description="Expiration time in days", ge=1, le=7)
60+
] = 7,
61+
s3_file_name: Annotated[
62+
str | None,
63+
Query(
64+
description="The S3 target file name. Defaults to '{account}-{identifier}.zip' if not provided."
65+
),
66+
] = None,
67+
) -> ActionSchemaOut:
68+
"""Get RDS logs for an instance.
69+
70+
This action retrieves logs from a specified RDS instance in a given AWS account and stores them in an S3 bucket.
71+
"""
72+
log.info(
73+
f"Getting logs for RDS {identifier} in AWS account {account}. action_id={action.action_id}"
74+
)
75+
external_resource_rds_logs_task.apply_async(
76+
kwargs={
77+
"account": account,
78+
"identifier": identifier,
79+
"expiration_days": expiration_days,
80+
"s3_file_name": s3_file_name,
81+
"action": action,
82+
},
83+
task_id=action.action_id,
84+
)
85+
return action.dump()
86+
87+
3088
def get_action_external_resource_rds_reboot(
3189
action_mgr: Annotated[ActionManager, Depends(get_action_manager)], user: UserDep
3290
) -> Action:

packages/automated_actions/automated_actions/celery/automated_action_task.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ def before_start( # noqa: PLR6301
2929

3030
def on_success( # noqa: PLR6301
3131
self,
32-
retval: Any, # noqa: ARG002
32+
retval: Any,
3333
task_id: str, # noqa: ARG002
3434
args: tuple, # noqa: ARG002
3535
kwargs: dict,
3636
) -> None:
37-
result = "ok"
37+
result = "ok" if retval is None else str(retval)
3838
kwargs["action"].set_final_state(
3939
status=ActionStatus.SUCCESS,
4040
result=result,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import logging
2+
3+
from automated_actions_utils.aws_api import (
4+
AWSApi,
5+
AWSStaticCredentials,
6+
LogStream,
7+
get_aws_credentials,
8+
)
9+
from automated_actions_utils.external_resource import (
10+
ExternalResource,
11+
ExternalResourceProvider,
12+
get_external_resource,
13+
)
14+
15+
from automated_actions.celery.app import app
16+
from automated_actions.celery.automated_action_task import AutomatedActionTask
17+
from automated_actions.config import settings
18+
from automated_actions.db.models import Action
19+
20+
log = logging.getLogger(__name__)
21+
22+
23+
class ExternalResourceRDSLogs:
24+
"""Class to handle RDS logs retrieval."""
25+
26+
def __init__(
27+
self, aws_api: AWSApi, rds: ExternalResource, s3_bucket: str, s3_prefix: str
28+
) -> None:
29+
self.aws_api = aws_api
30+
self.rds = rds
31+
self.s3_bucket = s3_bucket
32+
self.s3_prefix = s3_prefix
33+
34+
def run(
35+
self,
36+
target_aws_api: AWSApi,
37+
expiration_days: int,
38+
s3_file_name: str | None = None,
39+
) -> str | None:
40+
"""Retrieve RDS logs and upload them to S3 as a zip file."""
41+
s3_key = (
42+
s3_file_name
43+
or f"{self.s3_prefix}/{self.rds.account.name}-{self.rds.identifier}.zip"
44+
)
45+
# append .zip to the filename if not present
46+
if not s3_key.endswith(".zip"):
47+
s3_key += ".zip"
48+
49+
log.info(
50+
f"Saving RDS logs for {self.rds.account.name}/{self.rds.identifier} to S3 {self.s3_bucket}/{s3_key}"
51+
)
52+
log_streams = [
53+
LogStream(
54+
name=log_file,
55+
content=self.aws_api.stream_rds_log(
56+
identifier=self.rds.identifier, log_file=log_file
57+
),
58+
)
59+
for log_file in self.aws_api.list_rds_logs(self.rds.identifier)
60+
]
61+
if not log_streams:
62+
log.warning(
63+
f"No logs found for RDS {self.rds.identifier} in account {self.rds.account.name}"
64+
)
65+
return None
66+
self.aws_api.stream_rds_logs_to_s3_zip(
67+
log_streams=log_streams,
68+
bucket=self.s3_bucket,
69+
s3_key=s3_key,
70+
target_aws_api=target_aws_api,
71+
)
72+
return self.aws_api.generate_s3_download_url(
73+
bucket=self.s3_bucket,
74+
s3_key=s3_key,
75+
expiration_secs=expiration_days * 24 * 3600,
76+
)
77+
78+
79+
@app.task(base=AutomatedActionTask)
80+
def external_resource_rds_logs(
81+
account: str,
82+
identifier: str,
83+
expiration_days: int,
84+
action: Action, # noqa: ARG001
85+
s3_file_name: str | None = None,
86+
) -> str:
87+
rds = get_external_resource(
88+
account=account, identifier=identifier, provider=ExternalResourceProvider.RDS
89+
)
90+
rds_account_credentials = get_aws_credentials(
91+
vault_secret=rds.account.automation_token, region=rds.account.region
92+
)
93+
94+
log_account_credentials = AWSStaticCredentials(
95+
access_key_id=settings.external_resource_rds_logs.access_key_id,
96+
secret_access_key=settings.external_resource_rds_logs.secret_access_key,
97+
region=settings.external_resource_rds_logs.region,
98+
)
99+
100+
with (
101+
AWSApi(credentials=rds_account_credentials, region=rds.region) as aws_api,
102+
AWSApi(
103+
credentials=log_account_credentials,
104+
s3_endpoint_url=settings.external_resource_rds_logs.s3_url,
105+
) as log_aws_api,
106+
):
107+
url = ExternalResourceRDSLogs(
108+
aws_api=aws_api,
109+
rds=rds,
110+
s3_bucket=settings.external_resource_rds_logs.bucket,
111+
s3_prefix=settings.external_resource_rds_logs.prefix,
112+
).run(
113+
target_aws_api=log_aws_api,
114+
expiration_days=expiration_days,
115+
s3_file_name=s3_file_name,
116+
)
117+
118+
if not url:
119+
return "No logs found or no logs available for download."
120+
121+
return f"Download the RDS logs from the following URL: {url}. This link will expire in {expiration_days} days."

packages/automated_actions/automated_actions/celery/external_resource/tasks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
ExternalResourceFlushElastiCache,
33
external_resource_flush_elasticache,
44
)
5+
from ._rds_logs import ExternalResourceRDSLogs, external_resource_rds_logs
56
from ._rds_reboot import ExternalResourceRDSReboot, external_resource_rds_reboot
67
from ._rds_snapshot import ExternalResourceRDSSnapshot, external_resource_rds_snapshot
78

89
__all__ = [
910
"ExternalResourceFlushElastiCache",
11+
"ExternalResourceRDSLogs",
1012
"ExternalResourceRDSReboot",
1113
"ExternalResourceRDSSnapshot",
1214
"external_resource_flush_elasticache",
15+
"external_resource_rds_logs",
1316
"external_resource_rds_reboot",
1417
"external_resource_rds_snapshot",
1518
]

packages/automated_actions/automated_actions/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ class ExternalResourceElastiCacheConfig(BaseSettings):
2020
}
2121

2222

23+
class ExternalResourceRdsLogsConfig(BaseSettings):
24+
"""Configuration for the external resource RDS logs related actions."""
25+
26+
s3_url: str = "http://s3.localhost.localstack.cloud:4566"
27+
access_key_id: str = "localstack"
28+
secret_access_key: str = "localstack" # noqa: S105
29+
region: str = "us-east-1"
30+
bucket: str = "automated-actions"
31+
prefix: str = "rds-logs"
32+
33+
2334
class Settings(BaseSettings):
2435
# pydantic config
2536
model_config = {
@@ -77,6 +88,9 @@ class Settings(BaseSettings):
7788
external_resource_elasticache: ExternalResourceElastiCacheConfig = (
7889
ExternalResourceElastiCacheConfig()
7990
)
91+
external_resource_rds_logs: ExternalResourceRdsLogsConfig = (
92+
ExternalResourceRdsLogsConfig()
93+
)
8094

8195

8296
settings = Settings()

0 commit comments

Comments
 (0)