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.

Files changed (50) hide show
  1. pyxecm/__init__.py +2 -1
  2. pyxecm/avts.py +79 -33
  3. pyxecm/customizer/api/app.py +45 -796
  4. pyxecm/customizer/api/auth/__init__.py +1 -0
  5. pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
  6. pyxecm/customizer/api/auth/router.py +78 -0
  7. pyxecm/customizer/api/common/__init__.py +1 -0
  8. pyxecm/customizer/api/common/functions.py +47 -0
  9. pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
  10. pyxecm/customizer/api/common/models.py +21 -0
  11. pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
  12. pyxecm/customizer/api/common/router.py +72 -0
  13. pyxecm/customizer/api/settings.py +25 -0
  14. pyxecm/customizer/api/terminal/__init__.py +1 -0
  15. pyxecm/customizer/api/terminal/router.py +87 -0
  16. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  17. pyxecm/customizer/api/v1_csai/router.py +87 -0
  18. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  19. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  20. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  21. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  22. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  24. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  25. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  26. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  27. pyxecm/customizer/api/v1_payload/models.py +51 -0
  28. pyxecm/customizer/api/v1_payload/router.py +499 -0
  29. pyxecm/customizer/browser_automation.py +567 -324
  30. pyxecm/customizer/customizer.py +204 -430
  31. pyxecm/customizer/guidewire.py +907 -43
  32. pyxecm/customizer/k8s.py +243 -56
  33. pyxecm/customizer/m365.py +104 -15
  34. pyxecm/customizer/payload.py +1943 -885
  35. pyxecm/customizer/pht.py +19 -2
  36. pyxecm/customizer/servicenow.py +22 -5
  37. pyxecm/customizer/settings.py +9 -6
  38. pyxecm/helper/xml.py +69 -0
  39. pyxecm/otac.py +1 -1
  40. pyxecm/otawp.py +2104 -1535
  41. pyxecm/otca.py +569 -0
  42. pyxecm/otcs.py +202 -38
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/METADATA +6 -32
  45. pyxecm-2.0.2.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/WHEEL +1 -1
  47. pyxecm-2.0.0.dist-info/RECORD +0 -54
  48. /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
  49. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/licenses/LICENSE +0 -0
  50. {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.responses import JSONResponse
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 = payload.get("customizerSettings", {})
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
+ )