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.

Files changed (78) hide show
  1. pyxecm/__init__.py +7 -4
  2. pyxecm/avts.py +727 -254
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +163 -0
  9. pyxecm/customizer/api/auth/__init__.py +1 -0
  10. pyxecm/customizer/api/auth/functions.py +92 -0
  11. pyxecm/customizer/api/auth/models.py +13 -0
  12. pyxecm/customizer/api/auth/router.py +78 -0
  13. pyxecm/customizer/api/common/__init__.py +1 -0
  14. pyxecm/customizer/api/common/functions.py +47 -0
  15. pyxecm/customizer/api/common/metrics.py +92 -0
  16. pyxecm/customizer/api/common/models.py +21 -0
  17. pyxecm/customizer/api/common/payload_list.py +870 -0
  18. pyxecm/customizer/api/common/router.py +72 -0
  19. pyxecm/customizer/api/settings.py +128 -0
  20. pyxecm/customizer/api/terminal/__init__.py +1 -0
  21. pyxecm/customizer/api/terminal/router.py +87 -0
  22. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_csai/router.py +87 -0
  24. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  25. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  26. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  27. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  28. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  29. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  30. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  31. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  32. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  33. pyxecm/customizer/api/v1_payload/models.py +51 -0
  34. pyxecm/customizer/api/v1_payload/router.py +499 -0
  35. pyxecm/customizer/browser_automation.py +721 -286
  36. pyxecm/customizer/customizer.py +1076 -1425
  37. pyxecm/customizer/exceptions.py +35 -0
  38. pyxecm/customizer/guidewire.py +1186 -0
  39. pyxecm/customizer/k8s.py +901 -379
  40. pyxecm/customizer/log.py +107 -0
  41. pyxecm/customizer/m365.py +2967 -920
  42. pyxecm/customizer/nhc.py +1169 -0
  43. pyxecm/customizer/openapi.py +258 -0
  44. pyxecm/customizer/payload.py +18228 -7820
  45. pyxecm/customizer/pht.py +717 -286
  46. pyxecm/customizer/salesforce.py +516 -342
  47. pyxecm/customizer/sap.py +58 -41
  48. pyxecm/customizer/servicenow.py +611 -372
  49. pyxecm/customizer/settings.py +445 -0
  50. pyxecm/customizer/successfactors.py +408 -346
  51. pyxecm/customizer/translate.py +83 -48
  52. pyxecm/helper/__init__.py +5 -2
  53. pyxecm/helper/assoc.py +83 -43
  54. pyxecm/helper/data.py +2406 -870
  55. pyxecm/helper/logadapter.py +27 -0
  56. pyxecm/helper/web.py +229 -101
  57. pyxecm/helper/xml.py +596 -171
  58. pyxecm/maintenance_page/__init__.py +5 -0
  59. pyxecm/maintenance_page/__main__.py +6 -0
  60. pyxecm/maintenance_page/app.py +51 -0
  61. pyxecm/maintenance_page/settings.py +28 -0
  62. pyxecm/maintenance_page/static/favicon.avif +0 -0
  63. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  64. pyxecm/otac.py +235 -141
  65. pyxecm/otawp.py +2668 -1220
  66. pyxecm/otca.py +569 -0
  67. pyxecm/otcs.py +7956 -3237
  68. pyxecm/otds.py +2178 -925
  69. pyxecm/otiv.py +36 -21
  70. pyxecm/otmm.py +1272 -325
  71. pyxecm/otpd.py +231 -127
  72. pyxecm-2.0.1.dist-info/METADATA +122 -0
  73. pyxecm-2.0.1.dist-info/RECORD +76 -0
  74. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  75. pyxecm-1.6.dist-info/METADATA +0 -53
  76. pyxecm-1.6.dist-info/RECORD +0 -32
  77. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
  78. {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
+ )