pyxecm 2.0.0__py3-none-any.whl → 2.0.2__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.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +2 -1
- pyxecm/avts.py +79 -33
- pyxecm/customizer/api/app.py +45 -796
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +25 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +567 -324
- pyxecm/customizer/customizer.py +204 -430
- pyxecm/customizer/guidewire.py +907 -43
- pyxecm/customizer/k8s.py +243 -56
- pyxecm/customizer/m365.py +104 -15
- pyxecm/customizer/payload.py +1943 -885
- pyxecm/customizer/pht.py +19 -2
- pyxecm/customizer/servicenow.py +22 -5
- pyxecm/customizer/settings.py +9 -6
- pyxecm/helper/xml.py +69 -0
- pyxecm/otac.py +1 -1
- pyxecm/otawp.py +2104 -1535
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +202 -38
- pyxecm/otds.py +35 -13
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/METADATA +6 -32
- pyxecm-2.0.2.dist-info/RECORD +76 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/WHEEL +1 -1
- pyxecm-2.0.0.dist-info/RECORD +0 -54
- /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Define Models for the Maintenance Page Config."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MaintenanceModel(BaseModel):
|
|
7
|
+
"""Status object of the Maintenance Page."""
|
|
8
|
+
|
|
9
|
+
enabled: bool
|
|
10
|
+
title: str | None = ""
|
|
11
|
+
text: str | None = ""
|
|
12
|
+
footer: str | None = ""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Define router for v1_maintenance."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Form
|
|
7
|
+
|
|
8
|
+
from pyxecm.customizer.api.auth import models
|
|
9
|
+
from pyxecm.customizer.api.auth.functions import get_authorized_user
|
|
10
|
+
from pyxecm.customizer.api.common.functions import get_k8s_object
|
|
11
|
+
from pyxecm.customizer.api.v1_maintenance.functions import get_maintenance_mode_status, set_maintenance_mode_via_ingress
|
|
12
|
+
from pyxecm.customizer.api.v1_maintenance.models import MaintenanceModel
|
|
13
|
+
from pyxecm.customizer.k8s import K8s
|
|
14
|
+
from pyxecm.maintenance_page.settings import settings as maint_settings
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/v1/maintenance", tags=["maintenance"])
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("pyxecm.customizer.api.v1_maintenance")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get(path="")
|
|
22
|
+
async def status(
|
|
23
|
+
user: Annotated[models.User, Depends(get_authorized_user)], # noqa: ARG001
|
|
24
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
25
|
+
) -> MaintenanceModel:
|
|
26
|
+
"""Return status of maintenance mode.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
user (models.User):
|
|
30
|
+
Added to enforce authentication requirement
|
|
31
|
+
k8s_object (K8s):
|
|
32
|
+
K8s object instance of pyxecm K8s class
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
dict:
|
|
37
|
+
Details of maintenance mode.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
return get_maintenance_mode_status(k8s_object)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.post(path="")
|
|
45
|
+
async def set_maintenance_mode_options(
|
|
46
|
+
user: Annotated[models.User, Depends(get_authorized_user)], # noqa: ARG001
|
|
47
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
48
|
+
config: Annotated[MaintenanceModel, Form()],
|
|
49
|
+
) -> MaintenanceModel:
|
|
50
|
+
"""Configure the Maintenance Mode and set options.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
user (models.User):
|
|
54
|
+
Added to enforce authentication requirement
|
|
55
|
+
k8s_object (K8s):
|
|
56
|
+
K8s object instance of pyxecm K8s class
|
|
57
|
+
config (MaintenanceModel):
|
|
58
|
+
instance of the Maintenance Model
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
dict: _description_
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
# Enable / Disable the acutual Maintenance Mode
|
|
65
|
+
set_maintenance_mode_via_ingress(config.enabled, k8s_object)
|
|
66
|
+
|
|
67
|
+
if config.title:
|
|
68
|
+
maint_settings.title = config.title
|
|
69
|
+
|
|
70
|
+
if config.text:
|
|
71
|
+
maint_settings.text = config.text
|
|
72
|
+
|
|
73
|
+
if config.footer:
|
|
74
|
+
maint_settings.footer = config.footer
|
|
75
|
+
|
|
76
|
+
return get_maintenance_mode_status(k8s_object)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Define functions for v1_otcs."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from threading import Lock
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from pyxecm.customizer.api.settings import CustomizerAPISettings
|
|
10
|
+
from pyxecm.customizer.k8s import K8s
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/api/v1/otcs", tags=["otcs"])
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("pyxecm.customizer.api.v1_otcs")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def collect_otcs_logs(host: str, k8s_object: K8s, logs_lock: Lock, settings: CustomizerAPISettings) -> None:
|
|
18
|
+
"""Collect the logs for the given OTCS instance."""
|
|
19
|
+
|
|
20
|
+
with logs_lock:
|
|
21
|
+
timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d_%H-%M")
|
|
22
|
+
tgz_file = f"/tmp/{timestamp}_{host}.tar.gz" # noqa: S108
|
|
23
|
+
|
|
24
|
+
if host.startswith("otcs-frontend"):
|
|
25
|
+
container = "otcs-frontend-container"
|
|
26
|
+
elif host.startswith("otcs-backend-search"):
|
|
27
|
+
container = "otcs-backend-search-container"
|
|
28
|
+
elif host.startswith("otcs-admin"):
|
|
29
|
+
container = "otcs-admin-container"
|
|
30
|
+
else:
|
|
31
|
+
container = None
|
|
32
|
+
|
|
33
|
+
logger.info("Collecting logs for %s", host)
|
|
34
|
+
k8s_object.exec_pod_command(
|
|
35
|
+
pod_name=host,
|
|
36
|
+
command=["tar", "-czvf", tgz_file, "/opt/opentext/cs/logs", "/opt/opentext/cs_persist/contentserver.log"],
|
|
37
|
+
container=container,
|
|
38
|
+
timeout=1800,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger.info("Uploading logs for %s", host)
|
|
42
|
+
k8s_object.exec_pod_command(
|
|
43
|
+
pod_name=host,
|
|
44
|
+
command=[
|
|
45
|
+
"curl",
|
|
46
|
+
"-X",
|
|
47
|
+
"POST",
|
|
48
|
+
"-F",
|
|
49
|
+
f"file=@{tgz_file}",
|
|
50
|
+
f"{settings.upload_url}?key={settings.upload_key}",
|
|
51
|
+
],
|
|
52
|
+
container=container,
|
|
53
|
+
timeout=1800,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger.info("Cleanup logs for %s", host)
|
|
57
|
+
k8s_object.exec_pod_command(
|
|
58
|
+
pod_name=host,
|
|
59
|
+
command=["rm", tgz_file],
|
|
60
|
+
container=container,
|
|
61
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Define router for v1_otcs."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from threading import Lock, Thread
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import anyio
|
|
11
|
+
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
|
12
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
13
|
+
|
|
14
|
+
from pyxecm.customizer.api.auth.functions import get_authorized_user
|
|
15
|
+
from pyxecm.customizer.api.auth.models import User
|
|
16
|
+
from pyxecm.customizer.api.common.functions import get_k8s_object, get_otcs_logs_lock, get_settings
|
|
17
|
+
from pyxecm.customizer.api.settings import CustomizerAPISettings
|
|
18
|
+
from pyxecm.customizer.api.v1_otcs.functions import collect_otcs_logs
|
|
19
|
+
from pyxecm.customizer.k8s import K8s
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/api/v1/otcs", tags=["otcs"])
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("pyxecm.customizer.api.v1_otcs")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.put(path="/logs", tags=["otcs"])
|
|
27
|
+
async def put_otcs_logs(
|
|
28
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
29
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
30
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
31
|
+
otcs_logs_lock: Annotated[dict[str, Lock], Depends(get_otcs_logs_lock)],
|
|
32
|
+
hosts: Annotated[list[str], Body()],
|
|
33
|
+
) -> JSONResponse:
|
|
34
|
+
"""Collect the logs from the given OTCS instances."""
|
|
35
|
+
|
|
36
|
+
if "all" in hosts:
|
|
37
|
+
hosts = []
|
|
38
|
+
for sts in ["otcs-admin", "otcs-frontend", "otcs-backend-search"]:
|
|
39
|
+
try:
|
|
40
|
+
sts_replicas = k8s_object.get_stateful_set_scale(sts).status.replicas
|
|
41
|
+
hosts.extend([f"{sts}-{i}" for i in range(sts_replicas)])
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from e
|
|
44
|
+
|
|
45
|
+
msg = {}
|
|
46
|
+
for host in hosts:
|
|
47
|
+
if host not in otcs_logs_lock:
|
|
48
|
+
otcs_logs_lock[host] = Lock()
|
|
49
|
+
|
|
50
|
+
if not otcs_logs_lock[host].locked():
|
|
51
|
+
Thread(target=collect_otcs_logs, args=(host, k8s_object, otcs_logs_lock[host], settings)).start()
|
|
52
|
+
msg[host] = {"status": "ok", "message": "Logs are being collected"}
|
|
53
|
+
else:
|
|
54
|
+
msg[host] = {"status": "error", "message": "Logs are already being collected"}
|
|
55
|
+
|
|
56
|
+
status = (
|
|
57
|
+
HTTPStatus.TOO_MANY_REQUESTS if any(msg[host]["status"] == "error" for host in msg) else HTTPStatus.ACCEPTED
|
|
58
|
+
)
|
|
59
|
+
return JSONResponse(msg, status_code=status)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/logs/upload", tags=["otcs"], include_in_schema=True)
|
|
63
|
+
async def post_otcs_log_file(
|
|
64
|
+
file: Annotated[UploadFile, File(...)],
|
|
65
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
66
|
+
key: str = "",
|
|
67
|
+
) -> JSONResponse:
|
|
68
|
+
"""Upload a file to disk.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
file: File to be uploaded.
|
|
72
|
+
settings: CustomizerAPISettings.
|
|
73
|
+
key: Key to validate the upload.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
JSONResponse: Status of the upload
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
if key != settings.upload_key:
|
|
80
|
+
raise HTTPException(status_code=403, detail="Invalid Uploadkey")
|
|
81
|
+
|
|
82
|
+
os.makedirs(settings.upload_folder, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
async with await anyio.open_file(os.path.join(settings.upload_folder, file.filename), "wb") as f:
|
|
86
|
+
# Process the file in chunks instead of loading the entire file into memory
|
|
87
|
+
while True:
|
|
88
|
+
chunk = await file.read(65536) # Read 64KB at a time
|
|
89
|
+
if not chunk:
|
|
90
|
+
break
|
|
91
|
+
await f.write(chunk)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise HTTPException(status_code=500, detail="Something went wrong") from e
|
|
94
|
+
finally:
|
|
95
|
+
await file.close()
|
|
96
|
+
|
|
97
|
+
return {"message": f"Successfully uploaded {file.filename}"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/logs", tags=["otcs"])
|
|
101
|
+
async def get_otcs_log_files(
|
|
102
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
103
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
104
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
105
|
+
otcs_logs_lock: Annotated[dict[str, Lock], Depends(get_otcs_logs_lock)],
|
|
106
|
+
) -> JSONResponse:
|
|
107
|
+
"""List all otcs logs that can be downloaded."""
|
|
108
|
+
|
|
109
|
+
os.makedirs(settings.upload_folder, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
files = []
|
|
112
|
+
for filename in sorted(os.listdir(settings.upload_folder)):
|
|
113
|
+
file_path = os.path.join(settings.upload_folder, filename)
|
|
114
|
+
if os.path.isfile(file_path):
|
|
115
|
+
file_size = os.path.getsize(file_path)
|
|
116
|
+
files.append({"filename": filename, "size": file_size})
|
|
117
|
+
|
|
118
|
+
response = {"status": {host: bool(otcs_logs_lock[host].locked()) for host in otcs_logs_lock}, "files": files}
|
|
119
|
+
|
|
120
|
+
# Extend response with all hosts
|
|
121
|
+
for sts in ["otcs-admin", "otcs-frontend", "otcs-backend-search"]:
|
|
122
|
+
try:
|
|
123
|
+
sts_replicas = k8s_object.get_stateful_set_scale(sts).status.replicas
|
|
124
|
+
for i in range(sts_replicas):
|
|
125
|
+
host = f"{sts}-{i}"
|
|
126
|
+
|
|
127
|
+
if host in otcs_logs_lock:
|
|
128
|
+
response["status"][host] = otcs_logs_lock[host].locked()
|
|
129
|
+
else:
|
|
130
|
+
response["status"][host] = False
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from e
|
|
134
|
+
|
|
135
|
+
return JSONResponse(response, status_code=HTTPStatus.OK)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@router.delete("/logs", tags=["otcs"])
|
|
139
|
+
async def delete_otcs_log_files(
|
|
140
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
141
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
142
|
+
) -> JSONResponse:
|
|
143
|
+
"""Delete all otcs log files."""
|
|
144
|
+
shutil.rmtree(settings.upload_folder)
|
|
145
|
+
return JSONResponse({"message": "Successfully deleted all files"}, status_code=HTTPStatus.OK)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router.delete("/logs/{file_name}", tags=["otcs"])
|
|
149
|
+
async def delete_otcs_log_file(
|
|
150
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
151
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
152
|
+
file_name: str,
|
|
153
|
+
) -> FileResponse:
|
|
154
|
+
"""Delete single OTCS log archive."""
|
|
155
|
+
file_path = os.path.join(settings.upload_folder, file_name)
|
|
156
|
+
|
|
157
|
+
if not os.path.exists(file_path):
|
|
158
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
os.remove(file_path)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise HTTPException(status_code=500, detail=f"{e}") from e
|
|
164
|
+
|
|
165
|
+
return JSONResponse({"message": f"Successfully deleted {file_name}"}, status_code=HTTPStatus.OK)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@router.get("/logs/{file_name}", tags=["otcs"])
|
|
169
|
+
async def get_otcs_log_file(
|
|
170
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
171
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
172
|
+
file_name: str,
|
|
173
|
+
) -> FileResponse:
|
|
174
|
+
"""Download OTCS log archive."""
|
|
175
|
+
file_path = os.path.join(settings.upload_folder, file_name)
|
|
176
|
+
if not os.path.exists(file_path):
|
|
177
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
178
|
+
|
|
179
|
+
return FileResponse(file_path, media_type="application/octet-stream", filename=file_name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""API Implemenation for the Customizer to start and control the payload processing."""
|
|
2
|
+
|
|
3
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
4
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
5
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
6
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
7
|
+
__email__ = "mdiefenb@opentext.com"
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
13
|
+
|
|
14
|
+
import anyio
|
|
15
|
+
|
|
16
|
+
from pyxecm.customizer.api.common.functions import PAYLOAD_LIST
|
|
17
|
+
from pyxecm.customizer.api.settings import api_settings
|
|
18
|
+
from pyxecm.customizer.exceptions import PayloadImportError
|
|
19
|
+
from pyxecm.customizer.payload import load_payload
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("pyxecm.customizer.api.v1_payload")
|
|
22
|
+
|
|
23
|
+
# Initialize the globel Payloadlist object
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def import_payload(
|
|
27
|
+
payload: str | None = None,
|
|
28
|
+
payload_dir: str | None = None,
|
|
29
|
+
enabled: bool | None = None,
|
|
30
|
+
dependencies: bool | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Automatically load payload items from disk of a given directory.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
payload (str):
|
|
36
|
+
The name of the payload.
|
|
37
|
+
payload_dir (str):
|
|
38
|
+
The local path.
|
|
39
|
+
enabled (bool, optional):
|
|
40
|
+
Automatically start the processing (True), or only define items (False).
|
|
41
|
+
Defaults to False.
|
|
42
|
+
dependencies (bool, optional):
|
|
43
|
+
Automatically add dependency on the last payload in the queue
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def import_payload_file(
|
|
48
|
+
filename: str,
|
|
49
|
+
enabled: bool | None,
|
|
50
|
+
dependencies: bool | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if not os.path.isfile(filename):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if not (filename.endswith((".yaml", ".tfvars", ".tf", ".yml.gz.b64"))):
|
|
56
|
+
logger.debug("Skipping file: %s", filename)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Load payload file
|
|
60
|
+
payload_content = load_payload(filename)
|
|
61
|
+
if payload_content is None:
|
|
62
|
+
exception = f"The import of payload -> {filename} failed. Payload content could not be loaded."
|
|
63
|
+
raise PayloadImportError(exception)
|
|
64
|
+
|
|
65
|
+
payload_options = payload_content.get("payloadOptions", {})
|
|
66
|
+
|
|
67
|
+
if enabled is None:
|
|
68
|
+
enabled = payload_options.get("enabled", True)
|
|
69
|
+
|
|
70
|
+
# read name from options section if specified, otherwise take filename
|
|
71
|
+
name = payload_options.get("name", os.path.basename(filename))
|
|
72
|
+
|
|
73
|
+
# Get the loglevel from payloadOptions if set, otherwise use the default loglevel
|
|
74
|
+
loglevel = payload_options.get("loglevel", api_settings.loglevel)
|
|
75
|
+
|
|
76
|
+
# Get the git_url
|
|
77
|
+
git_url = payload_options.get("git_url", None)
|
|
78
|
+
|
|
79
|
+
# Dependency Management
|
|
80
|
+
if dependencies is None:
|
|
81
|
+
dependencies = []
|
|
82
|
+
|
|
83
|
+
# Get all dependencies from payloadOptions and resolve their ID
|
|
84
|
+
for dependency_name in payload_options.get("dependencies", []):
|
|
85
|
+
dependend_item = PAYLOAD_LIST.get_payload_item_by_name(dependency_name)
|
|
86
|
+
|
|
87
|
+
if dependend_item is None:
|
|
88
|
+
exception = (
|
|
89
|
+
f"The import of payload -> {name} failed. Dependencies cannot be resovled: {dependency_name}",
|
|
90
|
+
)
|
|
91
|
+
raise PayloadImportError(
|
|
92
|
+
exception,
|
|
93
|
+
)
|
|
94
|
+
# Add the ID to the list of dependencies
|
|
95
|
+
dependencies.append(dependend_item["index"])
|
|
96
|
+
|
|
97
|
+
elif dependencies:
|
|
98
|
+
try:
|
|
99
|
+
payload_items = len(PAYLOAD_LIST.get_payload_items()) - 1
|
|
100
|
+
dependencies = [payload_items] if payload_items != -1 else []
|
|
101
|
+
except Exception:
|
|
102
|
+
dependencies = []
|
|
103
|
+
else:
|
|
104
|
+
dependencies = []
|
|
105
|
+
|
|
106
|
+
customizer_settings = payload_content.get("customizerSettings", {})
|
|
107
|
+
|
|
108
|
+
logger.info("Adding payload: %s", filename)
|
|
109
|
+
payload = PAYLOAD_LIST.add_payload_item(
|
|
110
|
+
name=name,
|
|
111
|
+
filename=filename,
|
|
112
|
+
status="planned",
|
|
113
|
+
logfile=f"{api_settings.logfolder}/{name}.log",
|
|
114
|
+
dependencies=dependencies,
|
|
115
|
+
enabled=enabled,
|
|
116
|
+
git_url=git_url,
|
|
117
|
+
loglevel=loglevel,
|
|
118
|
+
customizer_settings=customizer_settings,
|
|
119
|
+
)
|
|
120
|
+
dependencies = payload["index"]
|
|
121
|
+
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if payload is None and payload_dir is None:
|
|
125
|
+
exception = "No payload or payload_dir provided"
|
|
126
|
+
raise ValueError(exception)
|
|
127
|
+
|
|
128
|
+
if payload and os.path.isdir(payload) and payload_dir is None:
|
|
129
|
+
payload_dir = payload
|
|
130
|
+
|
|
131
|
+
if payload_dir is None:
|
|
132
|
+
import_payload_file(payload, enabled, dependencies)
|
|
133
|
+
return
|
|
134
|
+
elif not os.path.isdir(payload_dir):
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
for filename in sorted(os.listdir(payload_dir)):
|
|
138
|
+
try:
|
|
139
|
+
import_payload_file(os.path.join(payload_dir, filename), enabled, dependencies)
|
|
140
|
+
except PayloadImportError:
|
|
141
|
+
logger.error("Payload import failed")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def prepare_dependencies(dependencies: list) -> list | None:
|
|
145
|
+
"""Convert the dependencies string to a list of integers."""
|
|
146
|
+
try:
|
|
147
|
+
list_all = dependencies[0].split(",")
|
|
148
|
+
except IndexError:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# Remove empty values from the list
|
|
152
|
+
items = list(filter(None, list_all))
|
|
153
|
+
converted_list = []
|
|
154
|
+
for item in items:
|
|
155
|
+
try:
|
|
156
|
+
converted_list.append(int(item))
|
|
157
|
+
except ValueError:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
return converted_list
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def tail_log(file_path: str) -> AsyncGenerator[str, None]:
|
|
164
|
+
"""Asynchronously follow the log file like `tail -f`."""
|
|
165
|
+
try:
|
|
166
|
+
async with await anyio.open_file(file_path) as file:
|
|
167
|
+
# Move the pointer to the end of the file
|
|
168
|
+
await file.seek(0, os.SEEK_END)
|
|
169
|
+
|
|
170
|
+
while True:
|
|
171
|
+
# Read new line
|
|
172
|
+
line = await file.readline()
|
|
173
|
+
if not line:
|
|
174
|
+
# Sleep for a little while before checking for new lines
|
|
175
|
+
await asyncio.sleep(0.5)
|
|
176
|
+
continue
|
|
177
|
+
yield line
|
|
178
|
+
except asyncio.exceptions.CancelledError:
|
|
179
|
+
pass
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Define Models for Payload."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PayloadStats(BaseModel):
|
|
9
|
+
"""Defines PayloadStats Model."""
|
|
10
|
+
|
|
11
|
+
count: int = 0
|
|
12
|
+
status: dict = {}
|
|
13
|
+
logs: dict = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PayloadListItem(BaseModel):
|
|
17
|
+
"""Defines PayloadListItem Model."""
|
|
18
|
+
|
|
19
|
+
index: int
|
|
20
|
+
name: str
|
|
21
|
+
filename: str
|
|
22
|
+
dependencies: list
|
|
23
|
+
logfile: str
|
|
24
|
+
status: str
|
|
25
|
+
enabled: bool
|
|
26
|
+
git_url: str | None
|
|
27
|
+
loglevel: str = "INFO"
|
|
28
|
+
start_time: Any | None
|
|
29
|
+
stop_time: Any | None
|
|
30
|
+
duration: Any | None
|
|
31
|
+
log_debug: int = 0
|
|
32
|
+
log_info: int = 0
|
|
33
|
+
log_warning: int = 0
|
|
34
|
+
log_error: int = 0
|
|
35
|
+
log_critical: int = 0
|
|
36
|
+
customizer_settings: dict = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PayloadListItems(BaseModel):
|
|
40
|
+
"""Defines PayloadListItems Model."""
|
|
41
|
+
|
|
42
|
+
stats: PayloadStats
|
|
43
|
+
results: list[PayloadListItem]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UpdatedPayloadListItem(BaseModel):
|
|
47
|
+
"""Defines UpdatedPayloadListItem Model."""
|
|
48
|
+
|
|
49
|
+
message: str
|
|
50
|
+
payload: PayloadListItem
|
|
51
|
+
updated_fields: dict
|