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 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -5,10 +5,9 @@ from typing import Annotated
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
8
|
-
from fastapi.
|
|
9
|
-
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
8
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
10
9
|
|
|
11
|
-
from pyxecm.customizer.api.models import User
|
|
10
|
+
from pyxecm.customizer.api.auth.models import User
|
|
12
11
|
from pyxecm.customizer.api.settings import api_settings
|
|
13
12
|
|
|
14
13
|
router = APIRouter()
|
|
@@ -91,64 +90,3 @@ async def get_authorized_user(current_user: Annotated[User, Depends(get_current_
|
|
|
91
90
|
detail=f"User {current_user.id} is not authorized",
|
|
92
91
|
)
|
|
93
92
|
return current_user
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@router.post("/token", tags=["auth"])
|
|
97
|
-
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> JSONResponse:
|
|
98
|
-
"""Login using OTDS and return a token."""
|
|
99
|
-
|
|
100
|
-
url = api_settings.otds_url + "/otdsws/rest/authentication/credentials"
|
|
101
|
-
|
|
102
|
-
payload = json.dumps(
|
|
103
|
-
{"userName": form_data.username, "password": form_data.password},
|
|
104
|
-
)
|
|
105
|
-
headers = {
|
|
106
|
-
"Content-Type": "application/json",
|
|
107
|
-
"Accept": "application/json",
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
response = requests.request(
|
|
112
|
-
"POST",
|
|
113
|
-
url,
|
|
114
|
-
headers=headers,
|
|
115
|
-
data=payload,
|
|
116
|
-
timeout=10,
|
|
117
|
-
)
|
|
118
|
-
except requests.exceptions.ConnectionError as exc:
|
|
119
|
-
raise HTTPException(
|
|
120
|
-
status_code=500,
|
|
121
|
-
detail=f"{exc.request.url} cannot be reached",
|
|
122
|
-
) from exc
|
|
123
|
-
|
|
124
|
-
if response.ok:
|
|
125
|
-
response = json.loads(response.text)
|
|
126
|
-
else:
|
|
127
|
-
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
|
128
|
-
|
|
129
|
-
return JSONResponse(
|
|
130
|
-
{
|
|
131
|
-
"access_token": response["ticket"],
|
|
132
|
-
"token_type": "bearer",
|
|
133
|
-
"userId": response["userId"],
|
|
134
|
-
},
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@router.get("/users/me", tags=["auth"])
|
|
139
|
-
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]) -> JSONResponse:
|
|
140
|
-
"""Get the current user.
|
|
141
|
-
|
|
142
|
-
current_user:
|
|
143
|
-
type: User
|
|
144
|
-
description: The current user.
|
|
145
|
-
|
|
146
|
-
"""
|
|
147
|
-
|
|
148
|
-
if "otadmins@otds.admin" in current_user.groups:
|
|
149
|
-
return JSONResponse(current_user.model_dump())
|
|
150
|
-
else:
|
|
151
|
-
raise HTTPException(
|
|
152
|
-
status_code=403,
|
|
153
|
-
detail=f"User {current_user.id} is not authorized",
|
|
154
|
-
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Utility library to handle the authentication with OTDS."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
10
|
+
|
|
11
|
+
from pyxecm.customizer.api.auth.functions import get_current_user
|
|
12
|
+
from pyxecm.customizer.api.auth.models import User
|
|
13
|
+
from pyxecm.customizer.api.settings import api_settings
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.post("/token", tags=["auth"])
|
|
21
|
+
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> JSONResponse:
|
|
22
|
+
"""Login using OTDS and return a token."""
|
|
23
|
+
|
|
24
|
+
url = api_settings.otds_url + "/otdsws/rest/authentication/credentials"
|
|
25
|
+
|
|
26
|
+
payload = json.dumps(
|
|
27
|
+
{"userName": form_data.username, "password": form_data.password},
|
|
28
|
+
)
|
|
29
|
+
headers = {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Accept": "application/json",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
response = requests.request(
|
|
36
|
+
"POST",
|
|
37
|
+
url,
|
|
38
|
+
headers=headers,
|
|
39
|
+
data=payload,
|
|
40
|
+
timeout=10,
|
|
41
|
+
)
|
|
42
|
+
except requests.exceptions.ConnectionError as exc:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=500,
|
|
45
|
+
detail=f"{exc.request.url} cannot be reached",
|
|
46
|
+
) from exc
|
|
47
|
+
|
|
48
|
+
if response.ok:
|
|
49
|
+
response = json.loads(response.text)
|
|
50
|
+
else:
|
|
51
|
+
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
|
52
|
+
|
|
53
|
+
return JSONResponse(
|
|
54
|
+
{
|
|
55
|
+
"access_token": response["ticket"],
|
|
56
|
+
"token_type": "bearer",
|
|
57
|
+
"userId": response["userId"],
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("/users/me", tags=["auth"])
|
|
63
|
+
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]) -> JSONResponse:
|
|
64
|
+
"""Get the current user.
|
|
65
|
+
|
|
66
|
+
current_user:
|
|
67
|
+
type: User
|
|
68
|
+
description: The current user.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if "otadmins@otds.admin" in current_user.groups:
|
|
73
|
+
return JSONResponse(current_user.model_dump())
|
|
74
|
+
else:
|
|
75
|
+
raise HTTPException(
|
|
76
|
+
status_code=403,
|
|
77
|
+
detail=f"User {current_user.id} is not authorized",
|
|
78
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""init module."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Define common functions."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pyxecm.customizer.api.common.payload_list import PayloadList
|
|
6
|
+
from pyxecm.customizer.api.settings import CustomizerAPISettings, api_settings
|
|
7
|
+
from pyxecm.customizer.k8s import K8s
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("pyxecm.customizer.api")
|
|
10
|
+
|
|
11
|
+
# Create a LOCK dict for singleton logs collection
|
|
12
|
+
LOGS_LOCK = {}
|
|
13
|
+
# Initialize the globel Payloadlist object
|
|
14
|
+
PAYLOAD_LIST = PayloadList(logger=logger)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_k8s_object() -> K8s:
|
|
18
|
+
"""Get an instance of a K8s object.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
K8s: Return a K8s object
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
return K8s(logger=logger, namespace=api_settings.namespace)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_settings() -> CustomizerAPISettings:
|
|
29
|
+
"""Get the API Settings object.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
CustomizerPISettings: Returns the API Settings
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
return api_settings
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_otcs_logs_lock() -> dict:
|
|
40
|
+
"""Get the Logs LOCK dict.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The dict with all LOCKS for the logs
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
return LOGS_LOCK
|
|
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
|
|
5
5
|
from prometheus_client import Gauge
|
|
6
6
|
from prometheus_fastapi_instrumentator.metrics import Info
|
|
7
7
|
|
|
8
|
-
from pyxecm.customizer.api.payload_list import PayloadList
|
|
8
|
+
from pyxecm.customizer.api.common.payload_list import PayloadList
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
## By Payload
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Define common base Models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomizerStatus(BaseModel):
|
|
9
|
+
"""Define Model for Customizer Status."""
|
|
10
|
+
|
|
11
|
+
version: int = 2
|
|
12
|
+
customizer_duration: Any | None
|
|
13
|
+
customizer_end_time: Any | None
|
|
14
|
+
customizer_start_time: Any | None
|
|
15
|
+
status_details: dict
|
|
16
|
+
status: str = "Stopped"
|
|
17
|
+
debug: int = 0
|
|
18
|
+
info: int = 0
|
|
19
|
+
warning: int = 0
|
|
20
|
+
error: int = 0
|
|
21
|
+
critical: int = 0
|
|
@@ -82,6 +82,7 @@ class PayloadList:
|
|
|
82
82
|
"log_warning",
|
|
83
83
|
"log_error",
|
|
84
84
|
"log_critical",
|
|
85
|
+
"customizer_settings",
|
|
85
86
|
],
|
|
86
87
|
)
|
|
87
88
|
|
|
@@ -222,6 +223,7 @@ class PayloadList:
|
|
|
222
223
|
enabled: bool = True,
|
|
223
224
|
git_url: str | None = None,
|
|
224
225
|
loglevel: str = "INFO",
|
|
226
|
+
customizer_settings: dict | None = None,
|
|
225
227
|
) -> dict:
|
|
226
228
|
"""Add a new item to the PayloadList.
|
|
227
229
|
|
|
@@ -243,6 +245,8 @@ class PayloadList:
|
|
|
243
245
|
Link to the payload in the GIT repository.
|
|
244
246
|
loglevel (str):
|
|
245
247
|
The log level for processing the payload. Either "INFO" or "DEBUG".
|
|
248
|
+
customizer_settings (dict):
|
|
249
|
+
Customizer settings for the payload. Defaults to None.
|
|
246
250
|
|
|
247
251
|
"""
|
|
248
252
|
|
|
@@ -260,6 +264,7 @@ class PayloadList:
|
|
|
260
264
|
"log_warning": 0,
|
|
261
265
|
"log_error": 0,
|
|
262
266
|
"log_critical": 0,
|
|
267
|
+
"customizer_settings": customizer_settings if customizer_settings else {},
|
|
263
268
|
}
|
|
264
269
|
self.payload_items = pd.concat(
|
|
265
270
|
[self.payload_items, pd.DataFrame([new_item])],
|
|
@@ -694,7 +699,7 @@ class PayloadList:
|
|
|
694
699
|
success = False
|
|
695
700
|
|
|
696
701
|
if payload:
|
|
697
|
-
customizer_settings =
|
|
702
|
+
customizer_settings = payload_item["customizer_settings"]
|
|
698
703
|
|
|
699
704
|
# Overwrite the customizer settings with the payload specific ones:
|
|
700
705
|
customizer_settings.update(
|
|
@@ -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)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import tempfile
|
|
5
|
+
import uuid
|
|
5
6
|
from typing import Literal
|
|
6
7
|
|
|
7
8
|
from pydantic import Field
|
|
@@ -21,6 +22,7 @@ class CustomizerAPISettings(BaseSettings):
|
|
|
21
22
|
)
|
|
22
23
|
bind_address: str = Field(default="0.0.0.0", description="Interface to bind the Customizer API.") # noqa: S104
|
|
23
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")
|
|
24
26
|
|
|
25
27
|
import_payload: bool = Field(default=False)
|
|
26
28
|
payload: str = Field(
|
|
@@ -89,6 +91,29 @@ class CustomizerAPISettings(BaseSettings):
|
|
|
89
91
|
description="Port of the VictoriaLogs Server",
|
|
90
92
|
)
|
|
91
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
|
+
|
|
92
117
|
model_config = SettingsConfigDict(env_prefix="CUSTOMIZER_")
|
|
93
118
|
|
|
94
119
|
def __init__(self, **data: any) -> None:
|
|
@@ -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
|
+
)
|