pyxecm 1.6__py3-none-any.whl → 2.0.1__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 +7 -4
- pyxecm/avts.py +727 -254
- pyxecm/coreshare.py +686 -467
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +163 -0
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/auth/functions.py +92 -0
- pyxecm/customizer/api/auth/models.py +13 -0
- 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/common/metrics.py +92 -0
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/common/payload_list.py +870 -0
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +128 -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 +721 -286
- pyxecm/customizer/customizer.py +1076 -1425
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +1186 -0
- pyxecm/customizer/k8s.py +901 -379
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2967 -920
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18228 -7820
- pyxecm/customizer/pht.py +717 -286
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +611 -372
- pyxecm/customizer/settings.py +445 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +596 -171
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +235 -141
- pyxecm/otawp.py +2668 -1220
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +7956 -3237
- pyxecm/otds.py +2178 -925
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.1.dist-info/METADATA +122 -0
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""API Implemenation for the Customizer to start and control the payload processing."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
10
|
+
from fastapi.responses import JSONResponse, RedirectResponse
|
|
11
|
+
|
|
12
|
+
from pyxecm.customizer.api.auth.functions import get_authorized_user
|
|
13
|
+
from pyxecm.customizer.api.auth.models import User
|
|
14
|
+
from pyxecm.customizer.api.common.functions import PAYLOAD_LIST
|
|
15
|
+
from pyxecm.customizer.api.common.models import CustomizerStatus
|
|
16
|
+
|
|
17
|
+
router = APIRouter(tags=["default"])
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("pyxecm.customizer.api.common")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/", include_in_schema=False)
|
|
23
|
+
async def redirect_to_api() -> RedirectResponse:
|
|
24
|
+
"""Redirect from / to /api.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
None
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
return RedirectResponse(url="/api")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get(path="/status", name="Get Status")
|
|
34
|
+
async def get_status() -> CustomizerStatus:
|
|
35
|
+
"""Get the status of the Customizer."""
|
|
36
|
+
|
|
37
|
+
df = PAYLOAD_LIST.get_payload_items()
|
|
38
|
+
|
|
39
|
+
if df is None:
|
|
40
|
+
raise HTTPException(
|
|
41
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
42
|
+
detail="Payload list is empty.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
all_status = df["status"].value_counts().to_dict()
|
|
46
|
+
|
|
47
|
+
return CustomizerStatus(
|
|
48
|
+
version=2,
|
|
49
|
+
customizer_duration=(all_status.get("running", None)),
|
|
50
|
+
customizer_end_time=None,
|
|
51
|
+
customizer_start_time=None,
|
|
52
|
+
status_details=all_status,
|
|
53
|
+
status="Running" if "running" in all_status else "Stopped",
|
|
54
|
+
debug=df["log_debug"].sum(),
|
|
55
|
+
info=df["log_info"].sum(),
|
|
56
|
+
warning=df["log_warning"].sum(),
|
|
57
|
+
error=df["log_error"].sum(),
|
|
58
|
+
critical=df["log_critical"].sum(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("/api/shutdown", include_in_schema=False)
|
|
63
|
+
def shutdown(user: Annotated[User, Depends(get_authorized_user)]) -> JSONResponse:
|
|
64
|
+
"""Endpoint to end the application."""
|
|
65
|
+
|
|
66
|
+
logger.warning(
|
|
67
|
+
"Shutting down the API - Requested via api by user -> %s",
|
|
68
|
+
user.id,
|
|
69
|
+
)
|
|
70
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
|
71
|
+
|
|
72
|
+
return JSONResponse({"status": "shutdown"}, status_code=HTTPStatus.ACCEPTED)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Settings for Customizer execution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from pydantic_settings import (
|
|
10
|
+
BaseSettings,
|
|
11
|
+
SettingsConfigDict,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Customzer Settings
|
|
16
|
+
class CustomizerAPISettings(BaseSettings):
|
|
17
|
+
"""Settings for the Customizer API."""
|
|
18
|
+
|
|
19
|
+
api_token: str | None = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Optional token that can be specified that has access to the Customizer API, bypassing the OTDS authentication.",
|
|
22
|
+
)
|
|
23
|
+
bind_address: str = Field(default="0.0.0.0", description="Interface to bind the Customizer API.") # noqa: S104
|
|
24
|
+
bind_port: int = Field(default=8000, description="Port to bind the Customizer API to")
|
|
25
|
+
workers: int = Field(default=1, description="Number of workers to use for the API BackgroundTasks")
|
|
26
|
+
|
|
27
|
+
import_payload: bool = Field(default=False)
|
|
28
|
+
payload: str = Field(
|
|
29
|
+
default="/payload/payload.yml.gz.b64",
|
|
30
|
+
description="Path to a single Payload file to be loaded.",
|
|
31
|
+
)
|
|
32
|
+
payload_dir: str = Field(
|
|
33
|
+
default="/payload-external/",
|
|
34
|
+
description="Path to a directory of Payload files. All files in this directory will be loaded in alphabetical order and dependencies will be added automatically on the previous object. So all payload in this folder will be processed sequentially in alphabetical oder.",
|
|
35
|
+
)
|
|
36
|
+
payload_dir_optional: str = Field(
|
|
37
|
+
default="/payload-optional/",
|
|
38
|
+
description="Path of Payload files to be loaded. No additional logic for dependencies will be applied, they need to be managed within the payloadSetitings section of each payload. See -> payloadOptions in the Payload Syntax documentation.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
temp_dir: str = Field(
|
|
42
|
+
default=os.path.join(tempfile.gettempdir(), "customizer"),
|
|
43
|
+
description="location of the temp folder. Used for temporary files during the payload execution",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
loglevel: Literal["INFO", "DEBUG", "WARNING", "ERROR"] = "INFO"
|
|
47
|
+
logfolder: str = Field(
|
|
48
|
+
default=os.path.join(tempfile.gettempdir(), "customizer"),
|
|
49
|
+
description="Logfolder for Customizer logfiles",
|
|
50
|
+
)
|
|
51
|
+
logfile: str = Field(
|
|
52
|
+
default="customizer.log",
|
|
53
|
+
description="Logfile for Customizer API. This logfile also contains the execution of every payload.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
namespace: str = Field(
|
|
57
|
+
default="default",
|
|
58
|
+
description="Namespace to use for otxecm resource lookups",
|
|
59
|
+
)
|
|
60
|
+
maintenance_mode: bool = Field(
|
|
61
|
+
default=True,
|
|
62
|
+
description="Automatically enable and disable the maintenance mode during payload deployments.",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
trusted_origins: list[str] = Field(
|
|
66
|
+
default=[
|
|
67
|
+
"http://localhost",
|
|
68
|
+
"http://localhost:5173",
|
|
69
|
+
"http://localhost:8080",
|
|
70
|
+
"https://manager.develop.terrarium.cloud",
|
|
71
|
+
"https://manager.terrarium.cloud",
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
otds_protocol: str = Field(default="http", alias="OTDS_PROTOCOL")
|
|
76
|
+
otds_host: str = Field(default="otds", alias="OTDS_HOSTNAME")
|
|
77
|
+
otds_port: int = Field(default=80, alias="OTDS_SERVICE_PORT_OTDS")
|
|
78
|
+
otds_url: str | None = Field(default=None, alias="OTDS_URL")
|
|
79
|
+
|
|
80
|
+
metrics: bool = Field(
|
|
81
|
+
default=True,
|
|
82
|
+
description="Enable or disable the /metrics endpoint for Prometheus",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
victorialogs_host: str = Field(
|
|
86
|
+
default="",
|
|
87
|
+
description="Hostname of the VictoriaLogs Server",
|
|
88
|
+
)
|
|
89
|
+
victorialogs_port: int = Field(
|
|
90
|
+
default=9428,
|
|
91
|
+
description="Port of the VictoriaLogs Server",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
csai: bool = Field(
|
|
95
|
+
default=True,
|
|
96
|
+
description="Enable the CSAI integration",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
csai_prefix: str = Field(
|
|
100
|
+
default="csai",
|
|
101
|
+
description="Prefix for the CSAI",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
upload_folder: str = Field(default=os.path.join(tempfile.gettempdir(), "upload"), description="Folder for uploads")
|
|
105
|
+
|
|
106
|
+
upload_key: str = Field(default=str(uuid.uuid4()), description="Upload key for the Logs")
|
|
107
|
+
|
|
108
|
+
upload_url: str = Field(
|
|
109
|
+
default="http://customizer:8000/api/v1/otcs/logs/upload", description="Upload URL for the Logs"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
ws_terminal: bool = Field(
|
|
113
|
+
default=False,
|
|
114
|
+
description="Enable the Websocket Terminal endpoint",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
model_config = SettingsConfigDict(env_prefix="CUSTOMIZER_")
|
|
118
|
+
|
|
119
|
+
def __init__(self, **data: any) -> None:
|
|
120
|
+
"""Class initializer."""
|
|
121
|
+
|
|
122
|
+
super().__init__(**data)
|
|
123
|
+
if self.otds_url is None:
|
|
124
|
+
self.otds_url = f"{self.otds_protocol}://{self.otds_host}:{self.otds_port}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Create Instance of settings
|
|
128
|
+
api_settings = CustomizerAPISettings()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""WebSocket Terminal Server defintion."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Define a websocket to connect to a shell session in a pod."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Query, WebSocket, WebSocketDisconnect, status
|
|
11
|
+
|
|
12
|
+
from pyxecm.customizer.api.auth.functions import get_authorized_user, get_current_user
|
|
13
|
+
|
|
14
|
+
router = APIRouter(tags=["terminal"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.websocket("/ws/terminal")
|
|
18
|
+
async def ws_terminal(websocket: WebSocket, pod: str = Query(...), command: str = Query(...)) -> None:
|
|
19
|
+
"""Websocket to connect to a shell session in a pod.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
websocket (WebSocket): WebSocket to connect to the shell session.
|
|
23
|
+
pod (str): pod name to connect to.
|
|
24
|
+
command (str): command to be executed.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
await websocket.accept()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Wait for the first message to be the token
|
|
31
|
+
token = await websocket.receive_text()
|
|
32
|
+
|
|
33
|
+
user = await get_current_user(token)
|
|
34
|
+
authrorized = await get_authorized_user(user)
|
|
35
|
+
|
|
36
|
+
if not authrorized:
|
|
37
|
+
await websocket.send_text("Invalid User: " + str(user))
|
|
38
|
+
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
except HTTPException:
|
|
42
|
+
await websocket.send_text("Invalid Token")
|
|
43
|
+
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
|
44
|
+
|
|
45
|
+
except WebSocketDisconnect:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
process = ["bash"] if pod == "customizer" else ["kubectl", "exec", "-it", pod, "--", command]
|
|
49
|
+
|
|
50
|
+
pid, fd = pty.fork()
|
|
51
|
+
if pid == 0:
|
|
52
|
+
subprocess.run(process, check=False) # noqa: ASYNC221
|
|
53
|
+
|
|
54
|
+
async def read_from_pty() -> None:
|
|
55
|
+
loop = asyncio.get_event_loop()
|
|
56
|
+
try:
|
|
57
|
+
while True:
|
|
58
|
+
data = await loop.run_in_executor(None, os.read, fd, 1024)
|
|
59
|
+
await websocket.send_text(data.decode(errors="ignore"))
|
|
60
|
+
except Exception: # noqa: S110
|
|
61
|
+
pass # PTY closed or WebSocket failed
|
|
62
|
+
|
|
63
|
+
async def write_to_pty() -> None:
|
|
64
|
+
try:
|
|
65
|
+
while True:
|
|
66
|
+
data = await websocket.receive_text()
|
|
67
|
+
os.write(fd, data.encode())
|
|
68
|
+
except Exception: # noqa: S110
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Launch read/write tasks
|
|
72
|
+
read_task = asyncio.create_task(read_from_pty())
|
|
73
|
+
write_task = asyncio.create_task(write_to_pty())
|
|
74
|
+
|
|
75
|
+
done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED)
|
|
76
|
+
|
|
77
|
+
# Cancel other task
|
|
78
|
+
for task in pending:
|
|
79
|
+
task.cancel()
|
|
80
|
+
|
|
81
|
+
try: # noqa: SIM105
|
|
82
|
+
os.kill(pid, signal.SIGKILL)
|
|
83
|
+
except ProcessLookupError:
|
|
84
|
+
pass # Already exited
|
|
85
|
+
|
|
86
|
+
with contextlib.suppress(Exception):
|
|
87
|
+
os.close(fd)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Define router for v1_maintenance."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Body, Depends
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from pyxecm.customizer.api.auth.functions import get_authorized_user
|
|
11
|
+
from pyxecm.customizer.api.auth.models import User
|
|
12
|
+
from pyxecm.customizer.api.common.functions import get_k8s_object, get_settings
|
|
13
|
+
from pyxecm.customizer.api.settings import CustomizerAPISettings
|
|
14
|
+
from pyxecm.customizer.k8s import K8s
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/v1/csai", tags=["csai"])
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("pyxecm.customizer.api.v1_csai")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("")
|
|
22
|
+
def get_csai_config_data(
|
|
23
|
+
user: Annotated[User, Depends(get_authorized_user)],
|
|
24
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
25
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
26
|
+
) -> JSONResponse:
|
|
27
|
+
"""Get the csai config data."""
|
|
28
|
+
|
|
29
|
+
logger.info("READ csai config data by user -> %s", user.id)
|
|
30
|
+
|
|
31
|
+
config_data = {}
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
csai_config_maps = [
|
|
35
|
+
cm for cm in k8s_object.list_config_maps().items if cm.metadata.name.startswith(settings.csai_prefix)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for config_map in csai_config_maps:
|
|
39
|
+
config_data[config_map.metadata.name] = config_map.data
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error("Could not read config data from k8s -> %s", e)
|
|
43
|
+
return JSONResponse({"status": "error", "message": str(e)})
|
|
44
|
+
|
|
45
|
+
return JSONResponse(config_data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.post("")
|
|
49
|
+
def set_csai_config_data(
|
|
50
|
+
user: Annotated[User, Depends(get_authorized_user)],
|
|
51
|
+
settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
|
|
52
|
+
k8s_object: Annotated[K8s, Depends(get_k8s_object)],
|
|
53
|
+
config: Annotated[dict, Body()],
|
|
54
|
+
) -> JSONResponse:
|
|
55
|
+
"""Get the csai config data."""
|
|
56
|
+
|
|
57
|
+
logger.info("READ csai config data by user -> %s", user.id)
|
|
58
|
+
|
|
59
|
+
for config_map in config:
|
|
60
|
+
if not config_map.startswith(settings.csai_prefix):
|
|
61
|
+
return JSONResponse(
|
|
62
|
+
{
|
|
63
|
+
"status": "error",
|
|
64
|
+
"message": f"Config map name {config_map} does not start with {settings.csai_prefix}",
|
|
65
|
+
},
|
|
66
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
for key, value in config.items():
|
|
71
|
+
logger.info("User: %s -> Replacing config map %s with %s", user.id, key, value)
|
|
72
|
+
k8s_object.replace_config_map(
|
|
73
|
+
config_map_name=key,
|
|
74
|
+
config_map_data=value,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error("Could not replace config map %s with %s -> %s", key, value, e)
|
|
79
|
+
return JSONResponse({"status": "error", "message": str(e)})
|
|
80
|
+
|
|
81
|
+
for deployment in ["chat-svc", "embed-svc", "embed-wrkr"]:
|
|
82
|
+
deployment = f"{settings.csai_prefix}-{deployment}"
|
|
83
|
+
|
|
84
|
+
logger.info("User: %s ->Restarting deployment -> %s", user.id, deployment)
|
|
85
|
+
k8s_object.restart_deployment(deployment)
|
|
86
|
+
|
|
87
|
+
return get_csai_config_data(user=user, k8s_object=k8s_object, settings=settings)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Define functions for v1_maintenance."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from fastapi import HTTPException
|
|
9
|
+
from pydantic import HttpUrl, ValidationError
|
|
10
|
+
|
|
11
|
+
from pyxecm.customizer.api.v1_maintenance.models import MaintenanceModel
|
|
12
|
+
from pyxecm.customizer.k8s import K8s
|
|
13
|
+
from pyxecm.maintenance_page.settings import settings as maint_settings
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("v1_maintenance.functions")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_cshost(k8s_object: K8s) -> str:
|
|
19
|
+
"""Get the cs_hostname from the environment Variable OTCS_PUBLIC_HOST otherwise read it from the otcs-frontend-configmap."""
|
|
20
|
+
|
|
21
|
+
if "OTCS_PUBLIC_URL" in os.environ:
|
|
22
|
+
return os.getenv("OTCS_PUBLIC_URL", "otcs")
|
|
23
|
+
|
|
24
|
+
else:
|
|
25
|
+
cm = k8s_object.get_config_map("otcs-frontend-configmap")
|
|
26
|
+
|
|
27
|
+
if cm is None:
|
|
28
|
+
raise HTTPException(
|
|
29
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
30
|
+
detail=f"Could not read otcs-frontend-configmap from namespace: {k8s_object.get_namespace()}",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
config_file = cm.data.get("config.yaml")
|
|
34
|
+
config = yaml.safe_load(config_file)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
cs_url = HttpUrl(config.get("csurl"))
|
|
38
|
+
except ValidationError as ve:
|
|
39
|
+
raise HTTPException(
|
|
40
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
41
|
+
detail="Could not read otcs_host from environment variable OTCS_PULIBC_URL or configmap otcs-frontend-configmap/config.yaml/cs_url",
|
|
42
|
+
) from ve
|
|
43
|
+
return cs_url.host
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_maintenance_mode_status(k8s_object: K8s) -> dict:
|
|
47
|
+
"""Get status of maintenance mode.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
dict:
|
|
51
|
+
Details of maintenance mode.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
ingress = k8s_object.get_ingress("otxecm-ingress")
|
|
55
|
+
|
|
56
|
+
if ingress is None:
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=500,
|
|
59
|
+
detail="No ingress object found to read Maintenance Mode status",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
enabled = False
|
|
63
|
+
for rule in ingress.spec.rules:
|
|
64
|
+
if rule.host == get_cshost(k8s_object):
|
|
65
|
+
enabled = rule.http.paths[0].backend.service.name != "otcs-frontend"
|
|
66
|
+
|
|
67
|
+
return MaintenanceModel(
|
|
68
|
+
enabled=enabled, title=maint_settings.title, text=maint_settings.text, footer=maint_settings.footer
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# return {
|
|
72
|
+
# "enabled": enabled,
|
|
73
|
+
# "title": maint_settings.title,
|
|
74
|
+
# "text": maint_settings.text,
|
|
75
|
+
# "footer": maint_settings.footer,
|
|
76
|
+
# }
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_maintenance_mode_via_ingress(enabled: bool, k8s_object: K8s) -> None:
|
|
80
|
+
"""Set maintenance mode."""
|
|
81
|
+
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Setting Maintenance Mode to -> %s",
|
|
84
|
+
(enabled),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if enabled:
|
|
88
|
+
k8s_object.update_ingress_backend_services(
|
|
89
|
+
"otxecm-ingress",
|
|
90
|
+
get_cshost(k8s_object=k8s_object),
|
|
91
|
+
"customizer",
|
|
92
|
+
5555,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
k8s_object.update_ingress_backend_services(
|
|
96
|
+
"otxecm-ingress",
|
|
97
|
+
get_cshost(k8s_object=k8s_object),
|
|
98
|
+
"otcs-frontend",
|
|
99
|
+
80,
|
|
100
|
+
)
|
|
@@ -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
|
+
)
|