Skip to content

Commit 4a38292

Browse files
authored
refactor: airflow context and utils (#27)
* feat: get Airflow context from env vars and set up job * feat: add base dag task and airflow settings models * feat: read aws params and inputs from job settings * fix: remove airflow email utils * feat: helper method to get job info from airflow settings in env * docs: update docstrings
1 parent 2615c99 commit 4a38292

11 files changed

Lines changed: 353 additions & 440 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
## Usage
1313
- This library contains code and classes that can be used globally for aind-airflow-service
1414
- Primarily used for submitting jobs to the HPC
15-
- The Docker image can used by Airflow KubernetesPodOperators in the aind-airflow-service to run tasks, for example:
15+
- The Docker image can be used by Airflow KubernetesPodOperators in the aind-airflow-service to run tasks, for example:
1616

1717
```python
18+
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
19+
1820
check_param_store_connection = KubernetesPodOperator(
1921
task_id="check_param_store_connection",
2022
image="ghcr.io/allenneuraldynamics/aind-airflow-jobs:latest",
2123
in_cluster=True,
2224
cmds=["python", "-m", "aind_airflow_jobs.dag_tasks.check_connections"],
23-
arguments=["check_param_store_connection"],
2425
env_vars={
25-
"EXAMPLE_ENV_VAR": "example_value",
26+
"AIRFLOW_CTX_TASK_ID": "{{ task.task_id }}",
2627
},
2728
)
2829
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Base class for running DAG tasks."""
2+
3+
import logging
4+
from typing import Optional
5+
6+
from aind_airflow_jobs.models import AirflowTaskSettings
7+
8+
9+
class DagTasks:
10+
"""Base class for running DAG tasks."""
11+
12+
def __init__(
13+
self, airflow_task_settings: Optional[AirflowTaskSettings] = None
14+
):
15+
"""Fetch Airflow context from environment variables"""
16+
self.airflow_task_settings = (
17+
AirflowTaskSettings()
18+
if airflow_task_settings is None
19+
else airflow_task_settings
20+
)
21+
22+
def run_task(self):
23+
"""Run the appropriate task based on Airflow task_id"""
24+
task_id = self.airflow_task_settings.ctx_task_id
25+
task_func = getattr(self, task_id, None)
26+
27+
if task_func is None or not callable(task_func):
28+
raise ValueError(
29+
f"Task function '{task_id}' not found or not callable!"
30+
)
31+
32+
task_func()
33+
logging.info(f"Task '{task_id}' completed successfully!")
Lines changed: 78 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""CLI module for check_connections DAG tasks."""
22

3-
import argparse
43
import base64
54
import logging
65
import os
@@ -9,104 +8,94 @@
98

109
import boto3
1110

12-
from aind_airflow_jobs.handlers.slurm_v2_handler import SlurmClientSettings
11+
from aind_airflow_jobs.dag_tasks.base import DagTasks
12+
from aind_airflow_jobs.models import SlurmClientSettings
1313

1414
logging.basicConfig(
1515
level=logging.INFO,
1616
stream=sys.stdout,
1717
)
1818

1919

20-
def check_param_store_connection():
21-
"""Check AWS parameter store connections."""
22-
# Airflow Variables and Connections must be passed as env vars
23-
default_transfer_settings = os.getenv("DEFAULT_TRANSFER_SETTINGS")
24-
slurm_uri = os.getenv("SLURM_URI")
25-
ams_uri = os.getenv("AMS_URI")
26-
co_uri = os.getenv("CO_URI")
20+
class CheckConnectionsDag(DagTasks):
21+
"""DAG tasks for checking connections to external services."""
2722

28-
logging.info(f"default_transfer_settings: {default_transfer_settings}")
29-
logging.info(f"ams_uri: {ams_uri}")
30-
31-
if not default_transfer_settings:
32-
raise AssertionError("Unable to retrieve default_transfer_settings!")
33-
if not slurm_uri:
34-
raise AssertionError("Unable to retrieve slurm_uri!")
35-
if not ams_uri:
36-
raise AssertionError("Unable to retrieve ams_uri!")
37-
if not co_uri:
38-
raise AssertionError("Unable to retrieve co_uri!")
39-
40-
41-
def check_aws_connection():
42-
"""Check AWS S3 connection."""
43-
44-
s3_bucket = os.getenv("S3_BUCKET")
45-
if not s3_bucket:
46-
raise AssertionError("S3_BUCKET environment variable not set!")
47-
48-
s3_client = boto3.client("s3")
49-
try:
50-
s3_client.list_objects_v2(
51-
Bucket=s3_bucket,
52-
MaxKeys=1,
23+
def check_param_store_connection(self):
24+
"""Check AWS parameter store connections."""
25+
# Airflow Variables and Connections must be passed as env vars
26+
default_transfer_settings = (
27+
self.airflow_task_settings.var_param_default
28+
)
29+
slurm_uri = os.getenv("SLURM_URI")
30+
ams_uri = os.getenv("AMS_URI")
31+
co_uri = os.getenv("CO_URI")
32+
33+
logging.info(f"default_transfer_settings: {default_transfer_settings}")
34+
logging.info(f"ams_uri: {ams_uri}")
35+
36+
if not default_transfer_settings:
37+
raise AssertionError(
38+
"Unable to retrieve default_transfer_settings!"
39+
)
40+
if not slurm_uri:
41+
raise AssertionError("Unable to retrieve slurm_uri!")
42+
if not ams_uri:
43+
raise AssertionError("Unable to retrieve ams_uri!")
44+
if not co_uri:
45+
raise AssertionError("Unable to retrieve co_uri!")
46+
47+
def check_aws_connection(self):
48+
"""Check AWS S3 connection."""
49+
50+
s3_bucket = self.airflow_task_settings.task_input_str
51+
if not s3_bucket:
52+
raise AssertionError("task_input_str must be set to S3 bucket!")
53+
54+
s3_client = boto3.client("s3")
55+
try:
56+
s3_client.list_objects_v2(
57+
Bucket=s3_bucket,
58+
MaxKeys=1,
59+
)
60+
finally:
61+
s3_client.close()
62+
63+
def check_slurm_connection(self):
64+
"""Check SLURM connection."""
65+
66+
settings = SlurmClientSettings()
67+
slurm_api = settings.create_api_client()
68+
response = slurm_api.slurm_v0040_get_ping()
69+
logging.info(f"SLURM ping response: {response}")
70+
71+
def check_vast_connection(self):
72+
"""Check that airflow can read files from VAST."""
73+
74+
logs_dir = self.airflow_task_settings.task_input_str
75+
if not logs_dir:
76+
raise AssertionError(
77+
"task_input_str must be set to VAST logs directory!"
78+
)
79+
80+
mounted_directory = logs_dir.replace("/allen/aind/", "/data/", 1)
81+
is_dir = Path(mounted_directory).is_dir()
82+
if not is_dir:
83+
raise NotADirectoryError(f"{mounted_directory} not recognized!")
84+
85+
def check_hpc_connection(self):
86+
"""Check hpc ssh command output can be read"""
87+
88+
# Airflow XCom output must be passed as env var
89+
ssh_command_output = self.airflow_task_settings.task_input_str or ""
90+
decoded = base64.b64decode(ssh_command_output).decode("utf-8")
91+
logging.info(
92+
f"SSH Command Output: {ssh_command_output}. Decoded: {decoded}"
5393
)
54-
finally:
55-
s3_client.close()
56-
57-
58-
def check_slurm_connection():
59-
"""Check SLURM connection."""
60-
61-
settings = SlurmClientSettings()
62-
slurm_api = settings.create_api_client()
63-
response = slurm_api.slurm_v0040_get_ping()
64-
logging.info(f"SLURM ping response: {response}")
65-
66-
67-
def check_vast_connection():
68-
"""Check that airflow can read files from VAST."""
69-
70-
logs_dir = os.getenv("SLURM_LOGS_DIR")
71-
if not logs_dir:
72-
raise AssertionError("SLURM_LOGS_DIR environment variable not set!")
73-
74-
mounted_directory = logs_dir.replace("/allen/aind/", "/data/", 1)
75-
is_dir = Path(mounted_directory).is_dir()
76-
if not is_dir:
77-
raise NotADirectoryError(f"{mounted_directory} not recognized!")
78-
79-
80-
def check_hpc_connection():
81-
"""Check hpc ssh command output can be read"""
82-
83-
# Airflow XCom output must be passed as env var
84-
ssh_command_output = os.getenv("SSH_COMMAND_OUTPUT", "")
85-
decoded_value = base64.b64decode(ssh_command_output).decode("utf-8")
86-
logging.info(
87-
f"SSH Command Output: {ssh_command_output}. Decoded: {decoded_value}"
88-
)
8994

90-
if decoded_value.strip() != "Hello World":
91-
raise AssertionError(f"Unexpected SSH command output: {decoded_value}")
95+
if decoded.strip() != "Hello World":
96+
raise AssertionError(f"Unexpected SSH command output: {decoded}")
9297

9398

9499
if __name__ == "__main__":
95-
parser = argparse.ArgumentParser(
96-
description="Run tasks for check_connections DAG"
97-
)
98-
parser.add_argument("task_id", help="Id of the task to run")
99-
100-
args = parser.parse_args()
101-
102-
# Get function by name from current module
103-
current_module = sys.modules[__name__]
104-
task_func = getattr(current_module, args.task_id, None)
105-
106-
if task_func is None or not callable(task_func):
107-
raise ValueError(
108-
f"Task function '{args.task_id}' not found or not callable!"
109-
)
110-
111-
task_func()
112-
logging.info(f"Task '{args.task_id}' completed successfully!")
100+
dag = CheckConnectionsDag()
101+
dag.run_task()

0 commit comments

Comments
 (0)