Skip to content

Commit 122c7d1

Browse files
committed
Rebase
Signed-off-by: Alina Buzachis <[email protected]>
1 parent 670be41 commit 122c7d1

19 files changed

+248
-416
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ repos:
5252
- id: mypy
5353
pass_filenames: false
5454
args: [--config-file=pyproject.toml, --ignore-missing-imports, "."]
55+
additional_dependencies:
56+
- types-requests

Dockerfile.dev

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ ADD pattern_service /app/pattern_service
1414

1515
COPY manage.py .
1616

17+
ENV PATTERN_SERVICE_MODE=development
18+
1719
EXPOSE 5000
1820

1921
ENV SQLITE_PATH=/tmp/db.sqlite3

core/controller_client.py

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import urllib.parse
66
from functools import wraps
77
from typing import Any
8+
from typing import Callable
89
from typing import Dict
910
from typing import List
1011
from typing import Optional
1112
from typing import Sequence
13+
from typing import TypeVar
1214

1315
import requests
1416
from requests import Session
@@ -29,28 +31,44 @@
2931

3032
settings = get_aap_settings()
3133

34+
F = TypeVar("F", bound=Callable[..., requests.Response])
35+
3236

3337
class RetryError(Exception):
3438
"""Custom exception raised when a retry limit is reached."""
3539

36-
def __init__(self, msg: str, request=None, response=None):
40+
def __init__(
41+
self, msg: str, request: Optional[Any] = None, response: Optional[Any] = None
42+
) -> None:
3743
super().__init__(msg)
3844
self.request = request
3945
self.response = response
4046

4147

4248
def build_collection_uri(collection: str, version: str) -> str:
49+
"""Builds the URI for a given collection and version."""
50+
base_url = settings.url
51+
path = "/api/galaxy/v3/plugin/ansible/content/published/collections/artifacts"
4352
filename = f"{collection}-{version}.tar.gz"
44-
return f"{settings.url}/api/galaxy/v3/plugin/ansible/content/published/collections/artifacts/{filename}"
53+
54+
return f"{base_url}{path}/{filename}"
4555

4656

47-
def wait_for_project_sync(project_id: str, *, max_retries: int = 15, initial_delay: float = 1, max_delay: float = 60, timeout: float = 30) -> None:
57+
def wait_for_project_sync(
58+
project_id: str,
59+
*,
60+
max_retries: int = 15,
61+
initial_delay: float = 1,
62+
max_delay: float = 60,
63+
timeout: float = 30,
64+
) -> None:
4865
"""
49-
Polls the AAP Controller project endpoint until the project sync completes successfully.
66+
Polls the AAP Controller project endpoint until the project sync completes
67+
successfully.
5068
51-
This function checks the sync status of a project using its ID. It will keep polling
52-
until the status becomes 'successful', or until a maximum number of retries is reached.
53-
Uses exponential backoff with jitter between retries.
69+
This function checks the sync status of a project using its ID. It will keep
70+
polling until the status becomes 'successful', or until a maximum number of
71+
retries is reached. Uses exponential backoff with jitter between retries.
5472
5573
Args:
5674
project_id (str): The numeric ID of the project to monitor.
@@ -65,7 +83,9 @@ def wait_for_project_sync(project_id: str, *, max_retries: int = 15, initial_del
6583
RequestException: For connection-related errors (e.g., network failures).
6684
"""
6785
session = get_http_session()
68-
url = urllib.parse.urljoin(settings.url, f"/api/controller/v2/projects/{project_id}")
86+
url = urllib.parse.urljoin(
87+
settings.url, f"/api/controller/v2/projects/{project_id}"
88+
)
6989
delay = initial_delay
7090

7191
for attempt in range(1, max_retries + 1):
@@ -74,22 +94,31 @@ def wait_for_project_sync(project_id: str, *, max_retries: int = 15, initial_del
7494
response.raise_for_status()
7595
status = response.json().get("status")
7696
if status == "successful":
77-
logger.info(f"Project {project_id} synced successfully on attempt {attempt}.")
97+
logger.info(
98+
f"Project {project_id} synced successfully on attempt {attempt}."
99+
)
78100
return
79101

80102
logger.info(f"Project {project_id} status: '{status}'. Retrying...")
81103

82104
except HTTPError as e:
83-
if e.response.status_code not in (408, 429) and 400 <= e.response.status_code < 500:
105+
if (
106+
e.response.status_code not in (408, 429)
107+
and 400 <= e.response.status_code < 500
108+
):
84109
raise
85-
logger.warning(f"Retryable HTTP error ({e.response.status_code}) on attempt {attempt}")
110+
logger.warning(
111+
f"Retryable HTTP error ({e.response.status_code}) on attempt {attempt}"
112+
)
86113
except (Timeout, RequestException) as e:
87114
logger.warning(f"Network error on attempt {attempt}: {e}")
88115
except Exception as e:
89116
logger.error(f"Unexpected error on attempt {attempt}: {e}")
90117

91118
if attempt == max_retries:
92-
raise RetryError(f"Project {project_id} failed to sync after {max_retries} attempts.")
119+
raise RetryError(
120+
f"Project {project_id} failed to sync after {max_retries} attempts."
121+
)
93122

94123
jitter = random.uniform(0.8, 1.2)
95124
sleep_time = min(delay * jitter, max_delay)
@@ -105,19 +134,19 @@ def get_http_session(force_refresh: bool = False) -> Session:
105134
session = Session()
106135
session.auth = HTTPBasicAuth(settings.username, settings.password)
107136
session.verify = settings.verify_ssl
108-
session.headers.update({'Content-Type': 'application/json'})
137+
session.headers.update({"Content-Type": "application/json"})
109138
_aap_session = session
110139
return _aap_session
111140

112141

113-
def safe_json(func):
142+
def safe_json(func: F) -> Callable[..., dict[str, Any]]:
114143
"""
115144
Decorator for functions that return a `requests.Response`.
116145
It attempts to parse JSON safely and falls back to raw text if needed.
117146
"""
118147

119148
@wraps(func)
120-
def wrapper(*args, **kwargs):
149+
def wrapper(*args: Any, **kwargs: Any) -> dict[str, Any]:
121150
response = func(*args, **kwargs)
122151
try:
123152
return response.json()
@@ -138,7 +167,7 @@ def post(
138167
data: Dict,
139168
*,
140169
dedupe_keys: Sequence[str] = ("name", "organization"),
141-
) -> Dict:
170+
) -> Dict[str, Any]:
142171
"""
143172
Create a resource on the AAP controller.
144173
If the POST fails with 400 because the object already exists,
@@ -166,18 +195,18 @@ def post(
166195
return safe_json(lambda: response)()
167196

168197
except requests.exceptions.HTTPError as exc:
169-
if exc.response.status_code != 400:
198+
response = exc.response
199+
if response.status_code != 400:
170200
raise
171201

172202
try:
173-
error_json = safe_json(lambda: exc.response)()
203+
error_json = safe_json(lambda: response)()
174204
except Exception:
175-
error_json = {"detail": exc.response.text}
205+
error_json = {"detail": response.text}
176206

177207
logger.warning(f"AAP POST {url} failed with 400. Error response: {error_json}")
178208
logger.debug(f"Payload sent: {json.dumps(data, indent=2)}")
179-
180-
logger.debug(f"AAP POST {url} returned 400. Attempting dedup lookup with keys {str(dedupe_keys)}")
209+
logger.debug(f"AAP POST {url} 400; dedup lookup keys: {dedupe_keys}")
181210

182211
# Attempt deduplication if resource already exists
183212
params = {k: data[k] for k in dedupe_keys if k in data}
@@ -187,15 +216,21 @@ def post(
187216
results = safe_json(lambda: lookup_resp)().get("results", [])
188217

189218
if results:
190-
logger.debug(f"Resource already exists. Returning existing resource: {results[0]}")
219+
logger.debug(
220+
f"Resource already exists. Returning existing resource: {results[0]}"
221+
)
191222
return results[0]
192223
except Exception as e:
193-
logger.debug(f"Deduplication GET failed for {url} with params {params}: {e}")
224+
logger.debug(
225+
f"Deduplication GET failed for {url} with params {params}: {e}"
226+
)
194227

195228
# If dedupe fails or no match found, raise with full detail
196229
raise requests.HTTPError(
197-
f"400 Bad Request for {url}.\n" f"Payload: {json.dumps(data, indent=2)}\n" f"Response: {json.dumps(error_json, indent=2)}",
198-
response=exc.response,
230+
f"400 Bad Request for {url}.\n"
231+
f"Payload: {json.dumps(data, indent=2)}\n"
232+
f"Response: {json.dumps(error_json, indent=2)}",
233+
response=response,
199234
)
200235

201236

@@ -207,7 +242,9 @@ def get(path: str, params: Optional[Dict] = None) -> requests.Response:
207242
return response
208243

209244

210-
def create_project(instance: PatternInstance, pattern: Pattern, pattern_def: Dict) -> int:
245+
def create_project(
246+
instance: PatternInstance, pattern: Pattern, pattern_def: Dict
247+
) -> int:
211248
"""
212249
Creates a controller project on AAP using the pattern definition.
213250
Args:
@@ -255,7 +292,9 @@ def create_execution_environment(instance: PatternInstance, pattern_def: Dict) -
255292
return post("/api/controller/v2/execution_environments/", ee_def)["id"]
256293

257294

258-
def create_labels(instance: PatternInstance, pattern_def: Dict) -> List[ControllerLabel]:
295+
def create_labels(
296+
instance: PatternInstance, pattern_def: Dict
297+
) -> List[ControllerLabel]:
259298
"""
260299
Creates controller labels and returns model instances.
261300
Args:
@@ -276,7 +315,9 @@ def create_labels(instance: PatternInstance, pattern_def: Dict) -> List[Controll
276315
return labels
277316

278317

279-
def create_job_templates(instance: PatternInstance, pattern_def: Dict, project_id: int, ee_id: int) -> List[Dict[str, Any]]:
318+
def create_job_templates(
319+
instance: PatternInstance, pattern_def: Dict, project_id: int, ee_id: int
320+
) -> List[Dict[str, Any]]:
280321
"""
281322
Creates job templates and associated surveys.
282323
Args:
@@ -299,7 +340,9 @@ def create_job_templates(instance: PatternInstance, pattern_def: Dict, project_i
299340
"organization": instance.organization_id,
300341
"project": project_id,
301342
"execution_environment": ee_id,
302-
"playbook": f"extensions/patterns/{pattern_def['name']}/playbooks/{jt['playbook']}",
343+
"playbook": (
344+
f"extensions/patterns/{pattern_def['name']}/playbooks/{jt['playbook']}"
345+
),
303346
"ask_inventory_on_launch": True,
304347
}
305348

@@ -309,15 +352,16 @@ def create_job_templates(instance: PatternInstance, pattern_def: Dict, project_i
309352

310353
if survey:
311354
logger.debug(f"Adding survey to job template {jt_id}")
312-
# post(f"/api/controller/v2/job_templates/{jt_id}/survey_spec/", {"spec": survey})
313355
post(f"/api/controller/v2/job_templates/{jt_id}/survey_spec/", survey)
314356

315357
automations.append({"type": "job_template", "id": jt_id, "primary": primary})
316358

317359
return automations
318360

319361

320-
def assign_execute_roles(executors: Dict[str, List[Any]], automations: List[Dict[str, Any]]) -> None:
362+
def assign_execute_roles(
363+
executors: Dict[str, List[Any]], automations: List[Dict[str, Any]]
364+
) -> None:
321365
"""
322366
Assigns JobTemplate Execute role to teams and users.
323367
Args:
@@ -328,7 +372,10 @@ def assign_execute_roles(executors: Dict[str, List[Any]], automations: List[Dict
328372
return
329373

330374
# Get role ID for "Execute" on JobTemplate
331-
result = get("/api/controller/v2/roles/", params={"name": "Execute", "content_type": "job_template"})
375+
result = get(
376+
"/api/controller/v2/roles/",
377+
params={"name": "Execute", "content_type": "job_template"},
378+
)
332379
roles_resp = result.json()
333380
if not roles_resp["results"]:
334381
raise ValueError("Could not find 'JobTemplate Execute' role.")

core/services.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
logger = logging.getLogger(__name__)
2929

3030

31-
def update_task_status(task: Task, status_: str, details: dict):
31+
def update_task_status(task: Task, status_: str, details: dict) -> None:
3232
"""
3333
Updates the status and details of a Task object.
3434
"""
@@ -49,10 +49,13 @@ def download_collection(collection: str, version: str) -> Iterator[str]:
4949
Yields:
5050
The path to the extracted collection files.
5151
"""
52-
path = f"/api/galaxy/v3/plugin/ansible/content/published/collections/artifacts/{collection}-{version}.tar.gz"
52+
filename = f"{collection}-{version}.tar.gz"
53+
base = "/api/galaxy/v3/plugin/ansible/content/published/collections/artifacts"
54+
path = f"{base}/{filename}"
5355
temp_base_dir = tempfile.mkdtemp()
5456
collection_path = os.path.join(temp_base_dir, f"{collection}-{version}")
5557
os.makedirs(collection_path, exist_ok=True)
58+
5659
try:
5760
response = get(path)
5861
in_memory_tar = io.BytesIO(response.content)
@@ -63,7 +66,13 @@ def download_collection(collection: str, version: str) -> Iterator[str]:
6366
shutil.rmtree(temp_base_dir)
6467

6568

66-
def save_instance_state(instance: PatternInstance, project_id: int, ee_id: int, labels: List[ControllerLabel], automations: List[Dict[str, Any]]) -> None:
69+
def save_instance_state(
70+
instance: PatternInstance,
71+
project_id: int,
72+
ee_id: int,
73+
labels: List[ControllerLabel],
74+
automations: List[Dict[str, Any]],
75+
) -> None:
6776
"""
6877
Saves the instance and links labels and automations inside a DB transaction.
6978
Args:
@@ -87,7 +96,7 @@ def save_instance_state(instance: PatternInstance, project_id: int, ee_id: int,
8796
)
8897

8998

90-
def pattern_task(pattern_id: int, task_id: int):
99+
def pattern_task(pattern_id: int, task_id: int) -> None:
91100
"""
92101
Orchestrates downloading a collection and saving a pattern definition.
93102
"""
@@ -97,15 +106,28 @@ def pattern_task(pattern_id: int, task_id: int):
97106
pattern = Pattern.objects.get(id=pattern_id)
98107
update_task_status(task, "Running", {"info": "Processing pattern"})
99108
collection_name: str = pattern.collection_name.replace(".", "-")
100-
with download_collection(collection_name, pattern.collection_version) as collection_path:
101-
path_to_definition = os.path.join(collection_path, "extensions", "patterns", pattern.pattern_name, "meta", "pattern.json")
109+
with download_collection(
110+
collection_name, pattern.collection_version
111+
) as collection_path:
112+
path_to_definition = os.path.join(
113+
collection_path,
114+
"extensions",
115+
"patterns",
116+
pattern.pattern_name,
117+
"meta",
118+
"pattern.json",
119+
)
102120
with open(path_to_definition, "r") as file:
103121
definition = json.load(file)
104122

105123
pattern.pattern_definition = definition
106-
pattern.collection_version_uri = build_collection_uri(collection_name, pattern.collection_version)
124+
pattern.collection_version_uri = build_collection_uri(
125+
collection_name, pattern.collection_version
126+
)
107127
pattern.save(update_fields=["pattern_definition", "collection_version_uri"])
108-
update_task_status(task, "Completed", {"info": "Pattern processed successfully"})
128+
update_task_status(
129+
task, "Completed", {"info": "Pattern processed successfully"}
130+
)
109131
except FileNotFoundError:
110132
logger.error(f"Could not find pattern definition for task {task_id}")
111133
update_task_status(task, "Failed", {"error": "Pattern definition not found."})
@@ -114,7 +136,7 @@ def pattern_task(pattern_id: int, task_id: int):
114136
update_task_status(task, "Failed", {"error": str(e)})
115137

116138

117-
def pattern_instance_task(instance_id: int, task_id: int):
139+
def pattern_instance_task(instance_id: int, task_id: int) -> None:
118140
task = Task.objects.get(id=task_id)
119141
try:
120142
instance = PatternInstance.objects.select_related("pattern").get(id=instance_id)

core/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
from core.services import pattern_task
33

44

5-
def run_pattern_task(pattern_id, task_id):
5+
def run_pattern_task(pattern_id: int, task_id: int) -> None:
66
pattern_task(pattern_id, task_id)
77

88

9-
def run_pattern_instance_task(instance_id, task_id):
9+
def run_pattern_instance_task(instance_id: int, task_id: int) -> None:
1010
pattern_instance_task(instance_id, task_id)

0 commit comments

Comments
 (0)