cpkit 0.1.0__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.
- cpkit/README.md +82 -0
- cpkit/__init__.py +42 -0
- cpkit/admin.py +53 -0
- cpkit/app.py +130 -0
- cpkit/audit/README.md +33 -0
- cpkit/audit/__init__.py +44 -0
- cpkit/audit/events_service.py +36 -0
- cpkit/audit/recorder.py +240 -0
- cpkit/audit/repository.py +64 -0
- cpkit/audit/router.py +46 -0
- cpkit/audit/service.py +34 -0
- cpkit/audit/types.py +38 -0
- cpkit/auth/README.md +38 -0
- cpkit/auth/__init__.py +123 -0
- cpkit/auth/api_key_router.py +52 -0
- cpkit/auth/api_key_service.py +143 -0
- cpkit/auth/api_keys.py +143 -0
- cpkit/auth/bundle.py +113 -0
- cpkit/auth/claims.py +37 -0
- cpkit/auth/config.py +162 -0
- cpkit/auth/dependencies.py +155 -0
- cpkit/auth/oidc.py +703 -0
- cpkit/auth/redirects.py +12 -0
- cpkit/auth/repositories.py +181 -0
- cpkit/auth/router.py +214 -0
- cpkit/auth/secrets.py +78 -0
- cpkit/auth/types.py +49 -0
- cpkit/bundle.py +252 -0
- cpkit/cli/README.md +21 -0
- cpkit/cli/__init__.py +14 -0
- cpkit/cli/__main__.py +5 -0
- cpkit/cli/base.py +200 -0
- cpkit/cli/schema.py +30 -0
- cpkit/cli/server.py +22 -0
- cpkit/config/README.md +13 -0
- cpkit/config/__init__.py +9 -0
- cpkit/config/env.py +31 -0
- cpkit/db/README.md +24 -0
- cpkit/db/__init__.py +23 -0
- cpkit/db/postgres.py +252 -0
- cpkit/dependencies.py +73 -0
- cpkit/errors/README.md +22 -0
- cpkit/errors/__init__.py +35 -0
- cpkit/errors/http.py +45 -0
- cpkit/errors/repository.py +32 -0
- cpkit/errors/service.py +74 -0
- cpkit/jobs/README.md +29 -0
- cpkit/jobs/__init__.py +49 -0
- cpkit/jobs/maintenance.py +15 -0
- cpkit/jobs/repository.py +306 -0
- cpkit/jobs/router.py +105 -0
- cpkit/jobs/service.py +138 -0
- cpkit/jobs/types.py +67 -0
- cpkit/jobs/worker.py +200 -0
- cpkit/logging/README.md +19 -0
- cpkit/logging/__init__.py +13 -0
- cpkit/logging/context.py +29 -0
- cpkit/logging/middleware.py +55 -0
- cpkit/logging/setup.py +81 -0
- cpkit/playbooks/README.md +25 -0
- cpkit/playbooks/__init__.py +40 -0
- cpkit/playbooks/ansible.py +359 -0
- cpkit/playbooks/repository.py +95 -0
- cpkit/playbooks/router.py +95 -0
- cpkit/playbooks/service.py +232 -0
- cpkit/playbooks/types.py +43 -0
- cpkit/repository.py +63 -0
- cpkit/resources/__init__.py +17 -0
- cpkit/resources/ddl.sql +150 -0
- cpkit/settings/README.md +22 -0
- cpkit/settings/__init__.py +18 -0
- cpkit/settings/keys.py +30 -0
- cpkit/settings/repository.py +108 -0
- cpkit/settings/router.py +62 -0
- cpkit/settings/service.py +105 -0
- cpkit/settings/types.py +25 -0
- cpkit/time.py +4 -0
- cpkit/webapp/README.md +71 -0
- cpkit/webapp/__init__.py +12 -0
- cpkit/webapp/index.html +970 -0
- cpkit/webapp/script.js +1690 -0
- cpkit/webapp/style.css +1561 -0
- cpkit-0.1.0.dist-info/METADATA +105 -0
- cpkit-0.1.0.dist-info/RECORD +90 -0
- cpkit-0.1.0.dist-info/WHEEL +4 -0
- cpkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- resources/README.md +16 -0
- resources/repository_maintenance_guide.md +90 -0
- resources/webapp_extension_guide.md +706 -0
- resources/webapp_extension_template.js +130 -0
cpkit/jobs/worker.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Generic queue worker loop."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from psycopg.rows import class_row
|
|
10
|
+
|
|
11
|
+
from cpkit.audit import job_id_ctx
|
|
12
|
+
|
|
13
|
+
from .maintenance import FAIL_ZOMBIE_JOBS_MESSAGE_TYPE, create_fail_zombie_jobs_handler
|
|
14
|
+
from .repository import QUEUE_TABLE
|
|
15
|
+
from .types import QueueMessage
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
QueueHandler = Callable[[int, Any, str], None]
|
|
20
|
+
ResolveHandler = Callable[[QueueMessage], QueueHandler | None]
|
|
21
|
+
ParseMessage = Callable[[QueueMessage], Any]
|
|
22
|
+
FailureHandler = Callable[[QueueMessage, Exception], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_queue_worker(
|
|
26
|
+
*,
|
|
27
|
+
get_pool: Callable[[], Any],
|
|
28
|
+
get_repo: Callable[[], Any],
|
|
29
|
+
handlers: Mapping[Any, QueueHandler] | None = None,
|
|
30
|
+
resolve_handler: ResolveHandler | None = None,
|
|
31
|
+
parse_message: ParseMessage | None = None,
|
|
32
|
+
handle_failure: FailureHandler | None = None,
|
|
33
|
+
failed_status: str = "FAILED",
|
|
34
|
+
) -> Callable[[], Any]:
|
|
35
|
+
"""Create a background task that polls the framework queue."""
|
|
36
|
+
maintenance_handlers = {
|
|
37
|
+
FAIL_ZOMBIE_JOBS_MESSAGE_TYPE: create_fail_zombie_jobs_handler(get_repo),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def record_failure(message: QueueMessage, err: Exception) -> None:
|
|
41
|
+
try:
|
|
42
|
+
get_repo().update_job(message.msg_id, failed_status)
|
|
43
|
+
except Exception:
|
|
44
|
+
logger.exception("Unable to mark job %s as failed", message.msg_id)
|
|
45
|
+
if handle_failure is not None:
|
|
46
|
+
handle_failure(message, err)
|
|
47
|
+
|
|
48
|
+
async def pull_from_mq() -> None:
|
|
49
|
+
await run_queue_worker(
|
|
50
|
+
get_pool=get_pool,
|
|
51
|
+
resolve_handler=_resolve_worker_handler(
|
|
52
|
+
maintenance_handlers,
|
|
53
|
+
handlers,
|
|
54
|
+
resolve_handler,
|
|
55
|
+
),
|
|
56
|
+
parse_message=_parse_worker_message(
|
|
57
|
+
maintenance_handlers,
|
|
58
|
+
parse_message,
|
|
59
|
+
),
|
|
60
|
+
handle_failure=record_failure,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return pull_from_mq
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_worker_handler(
|
|
67
|
+
maintenance_handlers: Mapping[str, QueueHandler],
|
|
68
|
+
handlers: Mapping[Any, QueueHandler] | None,
|
|
69
|
+
resolve_handler: ResolveHandler | None,
|
|
70
|
+
) -> ResolveHandler:
|
|
71
|
+
def resolve(message: QueueMessage) -> QueueHandler | None:
|
|
72
|
+
handler = maintenance_handlers.get(message.msg_type)
|
|
73
|
+
if handler is not None:
|
|
74
|
+
return handler
|
|
75
|
+
if handlers is not None:
|
|
76
|
+
handler = handlers.get(message.msg_type)
|
|
77
|
+
if handler is not None:
|
|
78
|
+
return handler
|
|
79
|
+
if resolve_handler is None:
|
|
80
|
+
return None
|
|
81
|
+
return resolve_handler(message)
|
|
82
|
+
|
|
83
|
+
return resolve
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_worker_message(
|
|
87
|
+
maintenance_handlers: Mapping[str, QueueHandler],
|
|
88
|
+
parse_message: ParseMessage | None,
|
|
89
|
+
) -> ParseMessage:
|
|
90
|
+
def parse(message: QueueMessage) -> Any:
|
|
91
|
+
if message.msg_type in maintenance_handlers:
|
|
92
|
+
return message.msg_data
|
|
93
|
+
if parse_message is None:
|
|
94
|
+
return message.msg_data
|
|
95
|
+
return parse_message(message)
|
|
96
|
+
|
|
97
|
+
return parse
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def run_queue_worker(
|
|
101
|
+
*,
|
|
102
|
+
get_pool: Callable[[], Any],
|
|
103
|
+
handlers: Mapping[Any, QueueHandler] | None = None,
|
|
104
|
+
resolve_handler: ResolveHandler | None = None,
|
|
105
|
+
parse_message: ParseMessage | None = None,
|
|
106
|
+
handle_failure: FailureHandler | None = None,
|
|
107
|
+
poll_seconds: float = 5,
|
|
108
|
+
jitter_min: float = 0.7,
|
|
109
|
+
jitter_max: float = 1.3,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Continuously claim due queue messages and dispatch them to handlers."""
|
|
112
|
+
try:
|
|
113
|
+
while True:
|
|
114
|
+
await asyncio.sleep(poll_seconds * random.uniform(jitter_min, jitter_max))
|
|
115
|
+
try:
|
|
116
|
+
with get_pool().connection() as conn:
|
|
117
|
+
with conn.cursor(row_factory=class_row(QueueMessage)) as cur:
|
|
118
|
+
with conn.transaction():
|
|
119
|
+
message = _claim_due_message(cur)
|
|
120
|
+
if message is None:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
logger.info(
|
|
124
|
+
"Processing queue message %s of type %s",
|
|
125
|
+
message.msg_id,
|
|
126
|
+
message.msg_type,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
job_id_token = _set_current_job_id(message)
|
|
130
|
+
try:
|
|
131
|
+
handler = _resolve_handler(
|
|
132
|
+
message,
|
|
133
|
+
handlers=handlers,
|
|
134
|
+
resolve_handler=resolve_handler,
|
|
135
|
+
)
|
|
136
|
+
payload = (
|
|
137
|
+
parse_message(message)
|
|
138
|
+
if parse_message is not None
|
|
139
|
+
else message.msg_data
|
|
140
|
+
)
|
|
141
|
+
handler(message.msg_id, payload, message.created_by)
|
|
142
|
+
except Exception as err:
|
|
143
|
+
logger.exception(
|
|
144
|
+
"Queue message %s failed during dispatch",
|
|
145
|
+
message.msg_id,
|
|
146
|
+
)
|
|
147
|
+
if handle_failure is not None:
|
|
148
|
+
try:
|
|
149
|
+
handle_failure(message, err)
|
|
150
|
+
except Exception:
|
|
151
|
+
logger.exception(
|
|
152
|
+
"Queue message %s failure hook failed",
|
|
153
|
+
message.msg_id,
|
|
154
|
+
)
|
|
155
|
+
finally:
|
|
156
|
+
if job_id_token is not None:
|
|
157
|
+
job_id_ctx.reset(job_id_token)
|
|
158
|
+
cur.execute(
|
|
159
|
+
f"DELETE FROM {QUEUE_TABLE} WHERE msg_id = %s;",
|
|
160
|
+
(message.msg_id,),
|
|
161
|
+
)
|
|
162
|
+
except Exception:
|
|
163
|
+
logger.exception("Unexpected failure while polling the message queue")
|
|
164
|
+
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
logger.info("Queue worker was stopped")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _claim_due_message(cur) -> QueueMessage | None:
|
|
170
|
+
return cur.execute(f"""
|
|
171
|
+
SELECT *
|
|
172
|
+
FROM {QUEUE_TABLE}
|
|
173
|
+
WHERE now() > start_after
|
|
174
|
+
LIMIT 1
|
|
175
|
+
FOR UPDATE SKIP LOCKED
|
|
176
|
+
""").fetchone()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _set_current_job_id(message: QueueMessage):
|
|
180
|
+
if message.msg_type == FAIL_ZOMBIE_JOBS_MESSAGE_TYPE:
|
|
181
|
+
return None
|
|
182
|
+
return job_id_ctx.set(message.msg_id)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_handler(
|
|
186
|
+
message: QueueMessage,
|
|
187
|
+
*,
|
|
188
|
+
handlers: Mapping[Any, QueueHandler] | None,
|
|
189
|
+
resolve_handler: ResolveHandler | None,
|
|
190
|
+
) -> QueueHandler:
|
|
191
|
+
if resolve_handler is not None:
|
|
192
|
+
handler = resolve_handler(message)
|
|
193
|
+
elif handlers is not None:
|
|
194
|
+
handler = handlers.get(message.msg_type)
|
|
195
|
+
else:
|
|
196
|
+
handler = None
|
|
197
|
+
|
|
198
|
+
if handler is None:
|
|
199
|
+
raise ValueError(f"Unknown task type requested: {message.msg_type}")
|
|
200
|
+
return handler
|
cpkit/logging/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Logging
|
|
2
|
+
|
|
3
|
+
The logging package configures operational logs and carries per-request context.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `context.py`: `request_id_ctx`, a logging filter, and compact formatter.
|
|
8
|
+
- `middleware.py`: FastAPI middleware that assigns/request-propagates
|
|
9
|
+
`X-Request-ID` and logs inbound/outbound requests.
|
|
10
|
+
- `setup.py`: Root logger setup, log levels, and journald-friendly formatting.
|
|
11
|
+
|
|
12
|
+
## Runtime Flow
|
|
13
|
+
|
|
14
|
+
1. `create_cpkit_app()` configures logging during startup.
|
|
15
|
+
2. Request middleware sets `request_id_ctx` for each HTTP request.
|
|
16
|
+
3. Audit recording uses `request_id_ctx.get` as the default request id provider.
|
|
17
|
+
4. Log formatters include the request id so API logs and audit records can be
|
|
18
|
+
correlated.
|
|
19
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Logging setup and request context helpers."""
|
|
2
|
+
|
|
3
|
+
from .context import RequestIDFilter, ShorthandFormatter, request_id_ctx
|
|
4
|
+
from .middleware import request_logging_middleware
|
|
5
|
+
from .setup import configure_logging
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"RequestIDFilter",
|
|
9
|
+
"ShorthandFormatter",
|
|
10
|
+
"configure_logging",
|
|
11
|
+
"request_logging_middleware",
|
|
12
|
+
"request_id_ctx",
|
|
13
|
+
]
|
cpkit/logging/context.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Request-aware logging context."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
|
|
6
|
+
request_id_ctx: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RequestIDFilter(logging.Filter):
|
|
10
|
+
def filter(self, record):
|
|
11
|
+
record.request_id = request_id_ctx.get() or "-"
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ShorthandFormatter(logging.Formatter):
|
|
16
|
+
LEVEL_MAP = {
|
|
17
|
+
"DEBUG": "D",
|
|
18
|
+
"INFO": "I",
|
|
19
|
+
"WARNING": "W",
|
|
20
|
+
"ERROR": "E",
|
|
21
|
+
"CRITICAL": "C",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def format(self, record):
|
|
25
|
+
original_levelname = record.levelname
|
|
26
|
+
record.levelname = self.LEVEL_MAP.get(original_levelname, original_levelname)
|
|
27
|
+
result = super().format(record)
|
|
28
|
+
record.levelname = original_levelname
|
|
29
|
+
return result
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""FastAPI request logging middleware helpers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import Request, Response
|
|
10
|
+
|
|
11
|
+
from .context import request_id_ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def request_logging_middleware(
|
|
15
|
+
request: Request,
|
|
16
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
17
|
+
) -> Response:
|
|
18
|
+
"""Attach a request id and log inbound/outbound request details."""
|
|
19
|
+
request_id = _request_id(request.headers.get("X-Request-ID"))
|
|
20
|
+
request_id_ctx.set(request_id)
|
|
21
|
+
|
|
22
|
+
start_time = time.perf_counter()
|
|
23
|
+
|
|
24
|
+
client = _client_address(request)
|
|
25
|
+
logging.debug(f'<- {client} - "{request.method} {request.url.path}"')
|
|
26
|
+
|
|
27
|
+
response = await call_next(request)
|
|
28
|
+
|
|
29
|
+
process_time_ms = (time.perf_counter() - start_time) * 1000
|
|
30
|
+
logging.info(
|
|
31
|
+
f'-> {client} - "{request.method} {request.url.path}" '
|
|
32
|
+
f"{response.status_code} | {process_time_ms:.2f}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
response.headers["X-Request-ID"] = request_id
|
|
36
|
+
response.headers["X-Process-Time-ms"] = f"{process_time_ms:.2f}"
|
|
37
|
+
|
|
38
|
+
return response
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _client_address(request: Request) -> str:
|
|
42
|
+
client: Any = request.client
|
|
43
|
+
if client is None:
|
|
44
|
+
return "-"
|
|
45
|
+
return f"{client.host}:{client.port}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _request_id(value: str | None) -> str:
|
|
49
|
+
normalized = (str(value).strip() or None) if value else None
|
|
50
|
+
if normalized is not None:
|
|
51
|
+
try:
|
|
52
|
+
return str(uuid.UUID(normalized))
|
|
53
|
+
except ValueError:
|
|
54
|
+
pass
|
|
55
|
+
return str(uuid.uuid4())
|
cpkit/logging/setup.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Logging configuration for operational messages."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cpkit.settings import FrameworkSettingKey
|
|
9
|
+
|
|
10
|
+
from .context import RequestIDFilter, ShorthandFormatter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def configure_logging(
|
|
14
|
+
settings_provider=None,
|
|
15
|
+
*,
|
|
16
|
+
force: bool = False,
|
|
17
|
+
default_level: str = "INFO",
|
|
18
|
+
default_journald_identifier: str = "app",
|
|
19
|
+
level_key: Any = FrameworkSettingKey.logging_level,
|
|
20
|
+
journald_identifier_key: Any = FrameworkSettingKey.logging_journald_identifier,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Configure root logging with journald when available."""
|
|
23
|
+
|
|
24
|
+
if getattr(configure_logging, "_configured", False) and not force:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
level_name = _setting_value(settings_provider, level_key, default_level).upper()
|
|
28
|
+
journald_identifier = _setting_value(
|
|
29
|
+
settings_provider,
|
|
30
|
+
journald_identifier_key,
|
|
31
|
+
default_journald_identifier,
|
|
32
|
+
)
|
|
33
|
+
level = getattr(logging, level_name, logging.INFO)
|
|
34
|
+
root_logger = logging.getLogger()
|
|
35
|
+
root_logger.setLevel(level)
|
|
36
|
+
logging.getLogger("uvicorn").setLevel(logging.ERROR)
|
|
37
|
+
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
|
|
38
|
+
|
|
39
|
+
if force:
|
|
40
|
+
for handler in list(root_logger.handlers):
|
|
41
|
+
root_logger.removeHandler(handler)
|
|
42
|
+
try:
|
|
43
|
+
handler.close()
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
elif root_logger.handlers:
|
|
47
|
+
configure_logging._configured = True
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
formatter = ShorthandFormatter(
|
|
51
|
+
"%(asctime)s [%(levelname)s] [%(request_id)s] %(message)s"
|
|
52
|
+
)
|
|
53
|
+
formatter.converter = time.gmtime
|
|
54
|
+
formatter.default_msec_format = "%s.%03d"
|
|
55
|
+
|
|
56
|
+
handler: logging.Handler
|
|
57
|
+
try:
|
|
58
|
+
if sys.platform != "linux":
|
|
59
|
+
raise RuntimeError("journald unavailable")
|
|
60
|
+
|
|
61
|
+
from systemd.journal import JournalHandler
|
|
62
|
+
|
|
63
|
+
handler = JournalHandler(SYSLOG_IDENTIFIER=journald_identifier)
|
|
64
|
+
except Exception:
|
|
65
|
+
handler = logging.StreamHandler()
|
|
66
|
+
|
|
67
|
+
handler.addFilter(RequestIDFilter())
|
|
68
|
+
handler.setFormatter(formatter)
|
|
69
|
+
root_logger.addHandler(handler)
|
|
70
|
+
configure_logging._configured = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _setting_value(settings_provider, key: Any, default: str) -> str:
|
|
74
|
+
if settings_provider is None:
|
|
75
|
+
return default
|
|
76
|
+
|
|
77
|
+
setting = settings_provider.get_setting(key)
|
|
78
|
+
value = getattr(setting, "value", setting)
|
|
79
|
+
if value in (None, ""):
|
|
80
|
+
return default
|
|
81
|
+
return str(value)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Playbooks
|
|
2
|
+
|
|
3
|
+
The playbooks package owns versioned playbook storage and Ansible execution.
|
|
4
|
+
It lets apps store playbook versions in the database, choose a default version,
|
|
5
|
+
edit playbooks from the admin webapp, and run the selected playbook as part of a
|
|
6
|
+
job.
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
- `types.py`: Playbook records, list/version responses, and save requests.
|
|
11
|
+
- `repository.py`: Repository mixin for playbook version storage and defaults.
|
|
12
|
+
- `service.py`: Version listing, loading, saving, deleting, and default-setting
|
|
13
|
+
rules.
|
|
14
|
+
- `router.py`: Admin playbook API routes.
|
|
15
|
+
- `ansible.py`: Ansible runner wrappers that load stored playbooks, update jobs,
|
|
16
|
+
and record task output.
|
|
17
|
+
|
|
18
|
+
## Runtime Flow
|
|
19
|
+
|
|
20
|
+
1. Admin APIs manage compressed playbook content and default versions.
|
|
21
|
+
2. A job handler calls `run_playbook()` or `run_playbook_lite()`.
|
|
22
|
+
3. The runner loads the default playbook version from the database.
|
|
23
|
+
4. The runner records the selected version on the job and writes task output as
|
|
24
|
+
Ansible events arrive.
|
|
25
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Versioned playbook models and repository helpers."""
|
|
2
|
+
|
|
3
|
+
from .ansible import (
|
|
4
|
+
AnsibleRunner,
|
|
5
|
+
LiteAnsibleRunner,
|
|
6
|
+
LiteRunnerResult,
|
|
7
|
+
RunnerResult,
|
|
8
|
+
run_playbook,
|
|
9
|
+
run_playbook_lite,
|
|
10
|
+
)
|
|
11
|
+
from .repository import PLAYBOOKS_TABLE, PlaybooksRepositoryMixin
|
|
12
|
+
from .router import create_playbooks_router
|
|
13
|
+
from .service import PlaybooksService
|
|
14
|
+
from .types import (
|
|
15
|
+
Playbook,
|
|
16
|
+
PlaybookListResponse,
|
|
17
|
+
PlaybookOverview,
|
|
18
|
+
PlaybookResponse,
|
|
19
|
+
PlaybookSaveRequest,
|
|
20
|
+
PlaybookVersionResponse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"PLAYBOOKS_TABLE",
|
|
25
|
+
"AnsibleRunner",
|
|
26
|
+
"LiteAnsibleRunner",
|
|
27
|
+
"LiteRunnerResult",
|
|
28
|
+
"Playbook",
|
|
29
|
+
"PlaybookListResponse",
|
|
30
|
+
"PlaybookOverview",
|
|
31
|
+
"PlaybookResponse",
|
|
32
|
+
"PlaybookSaveRequest",
|
|
33
|
+
"PlaybookVersionResponse",
|
|
34
|
+
"PlaybooksRepositoryMixin",
|
|
35
|
+
"PlaybooksService",
|
|
36
|
+
"RunnerResult",
|
|
37
|
+
"create_playbooks_router",
|
|
38
|
+
"run_playbook",
|
|
39
|
+
"run_playbook_lite",
|
|
40
|
+
]
|