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
@@ -1,15 +1,4 @@
1
- """API Implemenation for the Customizer to start and control the payload processing.
2
-
3
- Endpoints:
4
-
5
- GET /app/v1/payload - get processing list of payloads
6
- POST /app/v1/payload - add new payload to processing list
7
- GET /api/v1/payload/{payload_id} - get a specific payload
8
- GET /api/v1/payload/{payload_id}/content - get a specific payload content
9
- GET /api/v1/payload/{payload_id}/log - get a specific payload content
10
- GET /api/v1/payload/{payload_id}/log - get a specific payload content
11
-
12
- """
1
+ """API Implemenation for the Customizer to start and control the payload processing."""
13
2
 
14
3
  __author__ = "Dr. Marc Diefenbruch"
15
4
  __copyright__ = "Copyright (C) 2024-2025, OpenText"
@@ -19,33 +8,30 @@ __email__ = "mdiefenb@opentext.com"
19
8
 
20
9
  import logging
21
10
  import os
22
- import shutil
23
- import signal
24
11
  import sys
12
+ from collections.abc import AsyncGenerator
25
13
  from contextlib import asynccontextmanager
26
14
  from datetime import datetime, timezone
27
15
  from importlib.metadata import version
28
16
  from threading import Thread
29
- from typing import Annotated, Literal
30
17
 
31
18
  import uvicorn
32
- import yaml
33
- from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile
19
+ from fastapi import FastAPI
34
20
  from fastapi.middleware.cors import CORSMiddleware
35
- from fastapi.responses import FileResponse, JSONResponse, Response
36
- from fastapi.security import OAuth2PasswordBearer
37
21
  from prometheus_fastapi_instrumentator import Instrumentator
38
- from pydantic import HttpUrl, ValidationError
39
22
 
40
- from pyxecm.customizer import K8s
41
- from pyxecm.customizer.api import auth, models
42
- from pyxecm.customizer.api.metrics import payload_logs_by_payload, payload_logs_total
43
- from pyxecm.customizer.api.payload_list import PayloadList
23
+ from pyxecm.customizer.api.auth.router import router as auth_router
24
+ from pyxecm.customizer.api.common.functions import PAYLOAD_LIST
25
+ from pyxecm.customizer.api.common.metrics import payload_logs_by_payload, payload_logs_total
26
+ from pyxecm.customizer.api.common.router import router as common_router
44
27
  from pyxecm.customizer.api.settings import api_settings
45
- from pyxecm.customizer.exceptions import PayloadImportError
46
- from pyxecm.customizer.payload import load_payload
28
+ from pyxecm.customizer.api.terminal.router import router as terminal_router
29
+ from pyxecm.customizer.api.v1_csai.router import router as v1_csai_router
30
+ from pyxecm.customizer.api.v1_maintenance.router import router as v1_maintenance_router
31
+ from pyxecm.customizer.api.v1_otcs.router import router as v1_otcs_router
32
+ from pyxecm.customizer.api.v1_payload.functions import import_payload
33
+ from pyxecm.customizer.api.v1_payload.router import router as v1_payload_router
47
34
  from pyxecm.maintenance_page import run_maintenance_page
48
- from pyxecm.maintenance_page import settings as maint_settings
49
35
 
50
36
  # Check if Temp dir exists
51
37
  if not os.path.exists(api_settings.temp_dir):
@@ -60,7 +46,6 @@ if os.path.isfile(os.path.join(api_settings.logfolder, api_settings.logfile)):
60
46
  elif not os.path.exists(api_settings.logfolder):
61
47
  os.makedirs(api_settings.logfolder)
62
48
 
63
-
64
49
  handlers = [
65
50
  logging.FileHandler(os.path.join(api_settings.logfolder, api_settings.logfile)),
66
51
  logging.StreamHandler(sys.stdout),
@@ -75,9 +60,9 @@ logging.basicConfig(
75
60
 
76
61
 
77
62
  @asynccontextmanager
78
- async def lifespan( # noqa: ANN201
79
- app: FastAPI,
80
- ): # pylint: disable=unused-argument,redefined-outer-name
63
+ async def lifespan(
64
+ app: FastAPI, # noqa: ARG001
65
+ ) -> AsyncGenerator:
81
66
  """Lifespan Method for FASTAPI to handle the startup and shutdown process.
82
67
 
83
68
  Args:
@@ -86,10 +71,10 @@ async def lifespan( # noqa: ANN201
86
71
 
87
72
  """
88
73
 
89
- app.logger.debug("Settings -> %s", api_settings)
74
+ logger.debug("Settings -> %s", api_settings)
90
75
 
91
76
  if api_settings.import_payload:
92
- app.logger.info("Importing filesystem payloads...")
77
+ logger.info("Importing filesystem payloads...")
93
78
 
94
79
  # Base Payload
95
80
  import_payload(payload=api_settings.payload)
@@ -101,20 +86,20 @@ async def lifespan( # noqa: ANN201
101
86
  import_payload(payload_dir=api_settings.payload_dir_optional)
102
87
 
103
88
  if api_settings.maintenance_mode:
104
- app.logger.info("Starting maintenance_page thread...")
89
+ logger.info("Starting maintenance_page thread...")
105
90
  maint_thread = Thread(target=run_maintenance_page, name="maintenance_page")
106
91
  maint_thread.start()
107
92
 
108
- app.logger.info("Starting processing thread...")
93
+ logger.info("Starting processing thread...")
109
94
  thread = Thread(
110
- target=payload_list.run_payload_processing,
95
+ target=PAYLOAD_LIST.run_payload_processing,
111
96
  name="customization_run_api",
112
97
  )
113
98
  thread.start()
114
99
 
115
100
  yield
116
- app.logger.info("Shutdown")
117
- payload_list.stop_payload_processing()
101
+ logger.info("Shutdown")
102
+ PAYLOAD_LIST.stop_payload_processing()
118
103
 
119
104
 
120
105
  app = FastAPI(
@@ -124,10 +109,6 @@ app = FastAPI(
124
109
  lifespan=lifespan,
125
110
  version=version("pyxecm"),
126
111
  openapi_tags=[
127
- {
128
- "name": "status",
129
- "description": "Status of Customizer",
130
- },
131
112
  {
132
113
  "name": "auth",
133
114
  "description": "Authentication Endpoint - Users are authenticated against Opentext Directory Services",
@@ -142,7 +123,20 @@ app = FastAPI(
142
123
  },
143
124
  ],
144
125
  )
145
- app.logger = logging.getLogger("CustomizerAPI")
126
+
127
+ ## Add all Routers
128
+ app.include_router(router=common_router)
129
+ app.include_router(router=auth_router)
130
+ app.include_router(router=v1_maintenance_router)
131
+ app.include_router(router=v1_otcs_router)
132
+ app.include_router(router=v1_payload_router)
133
+ if api_settings.ws_terminal:
134
+ app.include_router(router=terminal_router)
135
+ if api_settings.csai:
136
+ app.include_router(router=v1_csai_router)
137
+
138
+
139
+ logger = logging.getLogger("CustomizerAPI")
146
140
  app.add_middleware(
147
141
  CORSMiddleware,
148
142
  allow_origins=api_settings.trusted_origins,
@@ -150,765 +144,20 @@ app.add_middleware(
150
144
  allow_methods=["*"],
151
145
  allow_headers=["*"],
152
146
  )
153
- app.k8s_object = K8s(logger=app.logger, namespace=api_settings.namespace)
154
- app.include_router(auth.router)
155
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
156
-
157
- # Initialize the globel Payloadlist object
158
- payload_list = PayloadList(logger=app.logger)
159
147
 
160
148
  if api_settings.metrics:
161
149
  # Add Prometheus Instrumentator for /metrics
162
150
  instrumentator = Instrumentator().instrument(app).expose(app)
163
- instrumentator.add(payload_logs_by_payload(payload_list))
164
- instrumentator.add(payload_logs_total(payload_list))
165
-
166
-
167
- @app.get(path="/status", name="Get Status", tags=["status"])
168
- async def get_status() -> dict:
169
- """Get the status of the Customizer."""
170
-
171
- df = payload_list.get_payload_items()
172
-
173
- if df is None:
174
- raise HTTPException(
175
- status_code=500,
176
- detail="Payload list is empty.",
177
- )
178
-
179
- all_status = df["status"].value_counts().to_dict()
180
-
181
- return {
182
- "version": "2",
183
- "customizer_duration": (all_status.get("running", None)),
184
- "customizer_end_time": None,
185
- "customizer_start_time": None,
186
- "status_details": all_status,
187
- "status": "Running" if "running" in all_status else "Stopped",
188
- "debug": df["log_debug"].sum(),
189
- "info": df["log_info"].sum(),
190
- "warning": df["log_warning"].sum(),
191
- "error": df["log_error"].sum(),
192
- "critical": df["log_critical"].sum(),
193
- }
194
-
195
-
196
- def import_payload(
197
- payload: str | None = None,
198
- payload_dir: str | None = None,
199
- enabled: bool | None = None,
200
- dependencies: bool | None = None,
201
- ) -> None:
202
- """Automatically load payload items from disk of a given directory.
203
-
204
- Args:
205
- payload (str):
206
- The name of the payload.
207
- payload_dir (str):
208
- The local path.
209
- enabled (bool, optional):
210
- Automatically start the processing (True), or only define items (False).
211
- Defaults to False.
212
- dependencies (bool, optional):
213
- Automatically add dependency on the last payload in the queue
214
-
215
- """
216
-
217
- def import_payload_file(
218
- filename: str,
219
- enabled: bool | None,
220
- dependencies: bool | None,
221
- ) -> None:
222
- if not os.path.isfile(filename):
223
- return
224
-
225
- if not (filename.endswith((".yaml", ".tfvars", ".tf", ".yml.gz.b64"))):
226
- app.logger.debug("Skipping file: %s", filename)
227
- return
228
-
229
- # Load payload file
230
- payload_content = load_payload(filename)
231
- if payload_content is None:
232
- exception = f"The import of payload -> {filename} failed. Payload content could not be loaded."
233
- raise PayloadImportError(exception)
234
-
235
- payload_options = payload_content.get("payloadOptions", {})
236
-
237
- if enabled is None:
238
- enabled = payload_options.get("enabled", True)
239
-
240
- # read name from options section if specified, otherwise take filename
241
- name = payload_options.get("name", os.path.basename(filename))
242
-
243
- # Get the loglevel from payloadOptions if set, otherwise use the default loglevel
244
- loglevel = payload_options.get("loglevel", api_settings.loglevel)
245
-
246
- # Get the git_url
247
- git_url = payload_options.get("git_url", None)
248
-
249
- # Dependency Management
250
- if dependencies is None:
251
- dependencies = []
252
-
253
- # Get all dependencies from payloadOptions and resolve their ID
254
- for dependency_name in payload_options.get("dependencies", []):
255
- dependend_item = payload_list.get_payload_item_by_name(dependency_name)
256
-
257
- if dependend_item is None:
258
- exception = (
259
- f"The import of payload -> {name} failed. Dependencies cannot be resovled: {dependency_name}",
260
- )
261
- raise PayloadImportError(
262
- exception,
263
- )
264
- # Add the ID to the list of dependencies
265
- dependencies.append(dependend_item["index"])
266
-
267
- elif dependencies:
268
- try:
269
- payload_items = len(payload_list.get_payload_items()) - 1
270
- dependencies = [payload_items] if payload_items != -1 else []
271
- except Exception:
272
- dependencies = []
273
- else:
274
- dependencies = []
275
-
276
- app.logger.info("Adding payload: %s", filename)
277
- payload = payload_list.add_payload_item(
278
- name=name,
279
- filename=filename,
280
- status="planned",
281
- logfile=f"{api_settings.logfolder}/{name}.log",
282
- dependencies=dependencies,
283
- enabled=enabled,
284
- git_url=git_url,
285
- loglevel=loglevel,
286
- )
287
- dependencies = payload["index"]
288
-
289
- return
290
-
291
- if payload is None and payload_dir is None:
292
- exception = "No payload or payload_dir provided"
293
- raise ValueError(exception)
294
-
295
- if payload and os.path.isdir(payload) and payload_dir is None:
296
- payload_dir = payload
297
-
298
- if payload_dir is None:
299
- import_payload_file(payload, enabled, dependencies)
300
- return
301
- elif not os.path.isdir(payload_dir):
302
- return
303
-
304
- for filename in sorted(os.listdir(payload_dir)):
305
- try:
306
- import_payload_file(os.path.join(payload_dir, filename), enabled, dependencies)
307
- except PayloadImportError:
308
- app.logger.error("Payload import failed")
309
-
310
-
311
- def prepare_dependencies(dependencies: list) -> list | None:
312
- """Convert the dependencies string to a list of integers."""
313
- try:
314
- list_all = dependencies[0].split(",")
315
- except IndexError:
316
- return None
317
-
318
- # Remove empty values from the list
319
- items = list(filter(None, list_all))
320
- converted_list = []
321
- for item in items:
322
- try:
323
- converted_list.append(int(item))
324
- except ValueError:
325
- continue
326
-
327
- return converted_list
328
-
329
-
330
- @app.post(path="/api/v1/payload", status_code=201, tags=["payload"])
331
- def create_payload_item(
332
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
333
- upload_file: Annotated[UploadFile, File(...)],
334
- name: Annotated[str, Form()] = "",
335
- dependencies: Annotated[list[int] | list[str] | None, Form()] = None,
336
- enabled: Annotated[bool, Form()] = True,
337
- loglevel: Annotated[
338
- Literal["DEBUG", "INFO", "WARNING"] | None,
339
- Form(
340
- description="Loglevel for the Payload processing",
341
- ),
342
- ] = "INFO",
343
- ) -> dict:
344
- """Upload a new payload item.
345
-
346
- Args:
347
- upload_file (UploadFile, optional):
348
- The file to upload. Defaults to File(...).
349
- name (str, optional):
350
- The name of the payload (if not provided we will use the file name).
351
- dependencies (list of integers):
352
- List of other payload items this item depends on.
353
- enabled (bool):
354
- Flag indicating if the payload is enabled or not.
355
- loglevel (str, optional):
356
- The loglevel for the payload processing. Defaults to "INFO".
357
- user (models.User, optional):
358
- The user who is uploading the payload. Defaults to None.
359
-
360
- Raises:
361
- HTTPException:
362
- Raised, if payload list is not initialized.
363
-
364
- Returns:
365
- dict:
366
- The HTTP response.
367
-
368
- """
369
- if dependencies:
370
- dependencies = prepare_dependencies(dependencies)
371
-
372
- # Set name if not provided
373
- name = name or os.path.splitext(os.path.basename(upload_file.filename))[0]
374
- file_extension = os.path.splitext(upload_file.filename)[1]
375
- file_name = os.path.join(api_settings.temp_dir, f"{name}{file_extension}")
376
-
377
- with open(file_name, "wb") as buffer:
378
- shutil.copyfileobj(upload_file.file, buffer)
379
-
380
- if dependencies == [-1]:
381
- dependencies = []
382
-
383
- return payload_list.add_payload_item(
384
- name=name,
385
- filename=file_name,
386
- status="planned",
387
- logfile=os.path.join(api_settings.temp_dir, "{}.log".format(name)),
388
- dependencies=dependencies or [],
389
- enabled=enabled,
390
- loglevel=loglevel,
391
- )
392
-
393
-
394
- @app.get(path="/api/v1/payload", tags=["payload"])
395
- async def get_payload_items(
396
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
397
- ) -> dict:
398
- """Get all Payload items.
399
-
400
- Raises:
401
- HTTPException: payload list not initialized
402
- HTTPException: payload list is empty
403
-
404
- Returns:
405
- dict:
406
- HTTP response with the result data
407
-
408
- """
409
-
410
- df = payload_list.get_payload_items()
411
-
412
- if df is None:
413
- raise HTTPException(
414
- status_code=500,
415
- detail="Payload list is empty.",
416
- )
417
-
418
- data = [{"index": idx, **row} for idx, row in df.iterrows()]
419
-
420
- stats = {
421
- "count": len(df),
422
- "status": df["status"].value_counts().to_dict(),
423
- "logs": {
424
- "debug": df["log_debug"].sum(),
425
- "info": df["log_info"].sum(),
426
- "warning": df["log_warning"].sum(),
427
- "error": df["log_error"].sum(),
428
- "critical": df["log_critical"].sum(),
429
- },
430
- }
431
-
432
- return {"stats": stats, "results": data}
433
-
434
-
435
- @app.get(path="/api/v1/payload/{payload_id}", tags=["payload"])
436
- async def get_payload_item(
437
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
438
- payload_id: int,
439
- ) -> dict:
440
- """Get a payload item based on its ID.
441
-
442
- Args:
443
- user: Annotated[models.User, Depends(auth.get_authorized_user)]
444
- payload_id (int): payload item ID
445
-
446
- Raises:
447
- HTTPException: a payload item with the given ID couldn't be found
448
-
449
- Returns:
450
- dict:
451
- HTTP response.
452
-
453
- """
454
- data = payload_list.get_payload_item(index=payload_id)
455
-
456
- if data is None:
457
- raise HTTPException(
458
- status_code=404,
459
- detail="Payload with index -> {} not found".format(payload_id),
460
- )
461
-
462
- return {"index": payload_id, **data}
463
-
464
-
465
- @app.put(path="/api/v1/payload/{payload_id}", status_code=200, tags=["payload"])
466
- async def update_payload_item(
467
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
468
- payload_id: int,
469
- name: Annotated[str | None, Form()] = None,
470
- dependencies: Annotated[list[int] | list[str] | None, Form()] = None,
471
- enabled: Annotated[bool | None, Form()] = None,
472
- status: Annotated[Literal["planned", "completed"] | None, Form()] = None,
473
- loglevel: Annotated[
474
- Literal["DEBUG", "INFO", "WARNING"] | None,
475
- Form(
476
- description="Loglevel for the Payload processing",
477
- ),
478
- ] = None,
479
- ) -> dict:
480
- """Update an existing payload item.
481
-
482
- Args:
483
- user (Optional[models.User]): User performing the update.
484
- payload_id (int): ID of the payload to update.
485
- upload_file (UploadFile, optional): replace the file name
486
- name (Optional[str]): Updated name.
487
- dependencies (Optional[List[int]]): Updated list of dependencies.
488
- enabled (Optional[bool]): Updated enabled status.
489
- loglevel (Optional[str]): Updated loglevel.
490
- status (Optional[str]): Updated status.
491
-
492
- Returns:
493
- dict: HTTP response with the updated payload details.
494
-
495
- """
496
-
497
- if dependencies:
498
- dependencies = prepare_dependencies(dependencies)
499
- # Check if the payload exists
500
- payload_item = payload_list.get_payload_item(
501
- payload_id,
502
- ) # Assumes a method to retrieve payload by ID
503
- if payload_item is None:
504
- raise HTTPException(
505
- status_code=404,
506
- detail="Payload with ID -> {} not found.".format(payload_id),
507
- )
508
-
509
- update_data = {}
510
-
511
- # Update fields if provided
512
- if name is not None:
513
- update_data["name"] = name
514
- if dependencies is not None:
515
- update_data["dependencies"] = dependencies
516
- if enabled is not None:
517
- update_data["enabled"] = enabled
518
- if status is not None:
519
- update_data["status"] = status
520
- if loglevel is not None:
521
- update_data["loglevel"] = loglevel
522
-
523
- if "status" in update_data and update_data["status"] == "planned":
524
- app.logger.info("Resetting log message counters for -> %s", payload_id)
525
- update_data["log_debug"] = 0
526
- update_data["log_info"] = 0
527
- update_data["log_warning"] = 0
528
- update_data["log_error"] = 0
529
- update_data["log_critical"] = 0
530
-
531
- update_data["start_time"] = None
532
- update_data["stop_time"] = None
533
- update_data["duration"] = None
534
-
535
- data = payload_list.get_payload_item(index=payload_id)
536
- if os.path.isfile(data.logfile):
537
- app.logger.info(
538
- "Deleting log file (for payload) -> %s (%s)",
539
- data.logfile,
540
- payload_id,
541
- )
542
-
543
- now = datetime.now(timezone.utc)
544
- old_log_name = (
545
- os.path.dirname(data.logfile)
546
- + "/"
547
- + os.path.splitext(os.path.basename(data.logfile))[0]
548
- + now.strftime("_%Y-%m-%d_%H-%M-%S.log")
549
- )
550
-
551
- os.rename(data.logfile, old_log_name)
552
-
553
- # Save the updated payload back to the list (or database)
554
- result = payload_list.update_payload_item(
555
- index=payload_id,
556
- update_data=update_data,
557
- ) # Assumes a method to update the payload
558
- if not result:
559
- raise HTTPException(
560
- status_code=404,
561
- detail="Failed to update Payload with ID -> {} with data -> {}".format(
562
- payload_id,
563
- update_data,
564
- ),
565
- )
566
-
567
- return {
568
- "message": "Payload updated successfully",
569
- "payload": {**payload_list.get_payload_item(index=payload_id)},
570
- "updated_fields": update_data,
571
- }
572
-
573
-
574
- @app.delete(path="/api/v1/payload/{payload_id}", status_code=204, tags=["payload"])
575
- async def delete_payload_item(
576
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
577
- payload_id: int,
578
- ) -> JSONResponse:
579
- """Delete an existing payload item.
580
-
581
- Args:
582
- user (Optional[models.User]): User performing the update.
583
- payload_id (int): The ID of the payload to update.
584
-
585
- Returns:
586
- dict: response or None
587
-
588
- """
589
-
590
- # Check if the payload exists
591
- result = payload_list.remove_payload_item(payload_id)
592
- if not result:
593
- raise HTTPException(
594
- status_code=404,
595
- detail="Payload with ID -> {} not found.".format(payload_id),
596
- )
597
-
598
-
599
- @app.put(path="/api/v1/payload/{payload_id}/up", tags=["payload"])
600
- async def move_payload_item_up(
601
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
602
- payload_id: int,
603
- ) -> dict:
604
- """Move a payload item up in the list.
605
-
606
- Args:
607
- user: Annotated[models.User, Depends(auth.get_authorized_user)]
608
- payload_id (int): payload item ID
609
-
610
- Raises:
611
- HTTPException: a payload item with the given ID couldn't be found
612
-
613
- Returns:
614
- dict: HTTP response
615
-
616
- """
617
-
618
- position = payload_list.move_payload_item_up(index=payload_id)
619
-
620
- if position is None:
621
- raise HTTPException(
622
- status_code=404,
623
- detail="Payload item with index -> {} is either out of range or is already on top of the payload list!".format(
624
- payload_id,
625
- ),
626
- )
627
-
628
- return {"result": {"new_position": position}}
629
-
630
-
631
- @app.put(path="/api/v1/payload/{payload_id}/down", tags=["payload"])
632
- async def move_payload_item_down(
633
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
634
- payload_id: int,
635
- ) -> dict:
636
- """Move a payload item down in the list.
637
-
638
- Args:
639
- user: Annotated[models.User, Depends(auth.get_authorized_user)]
640
- payload_id (int):
641
- The payload item ID.
642
-
643
- Raises:
644
- HTTPException: a payload item with the given ID couldn't be found
645
-
646
- Returns:
647
- dict: HTTP response
648
-
649
- """
650
-
651
- position = payload_list.move_payload_item_down(index=payload_id)
652
-
653
- if position is None:
654
- raise HTTPException(
655
- status_code=404,
656
- detail="Payload item with index -> {} is either out of range or is already on bottom of the payload list!".format(
657
- payload_id,
658
- ),
659
- )
660
-
661
- return {"result": {"new_position": position}}
662
-
663
-
664
- @app.get(path="/api/v1/payload/{payload_id}/content", tags=["payload"])
665
- async def get_payload_content(
666
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
667
- # pylint: disable=unused-argument
668
- payload_id: int,
669
- ) -> dict | None:
670
- """Get a payload item based on its ID.
671
-
672
- Args:
673
- user: Annotated[models.User, Depends(auth.get_authorized_user)]
674
- payload_id (int):
675
- The payload item ID.
676
-
677
- Raises:
678
- HTTPException:
679
- A payload item with the given ID couldn't be found.
680
-
681
- Returns:
682
- dict:
683
- HTTP response.
684
-
685
- """
686
-
687
- data = payload_list.get_payload_item(index=payload_id)
688
-
689
- if data is None:
690
- raise HTTPException(
691
- status_code=404,
692
- detail="Payload with ID -> {} not found!".format(payload_id),
693
- )
694
-
695
- filename = data.filename
696
-
697
- return load_payload(payload_source=filename)
698
-
699
-
700
- @app.get(path="/api/v1/payload/{payload_id}/download", tags=["payload"])
701
- def download_payload_content(
702
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
703
- payload_id: int,
704
- ) -> FileResponse:
705
- """Download the payload for a specific payload item."""
706
-
707
- payload = payload_list.get_payload_item(index=payload_id)
708
-
709
- if payload is None:
710
- raise HTTPException(
711
- status_code=404,
712
- detail="Payload with ID -> {} not found!".format(payload_id),
713
- )
714
-
715
- if not os.path.isfile(payload.filename):
716
- raise HTTPException(
717
- status_code=404,
718
- detail="Payload file -> '{}' not found".format(payload.filename),
719
- )
720
- with open(payload.filename, encoding="UTF-8") as file:
721
- content = file.read()
722
- return Response(
723
- content,
724
- media_type="application/octet-stream",
725
- headers={
726
- "Content-Disposition": f'attachment; filename="{os.path.basename(payload.filename)}"',
727
- },
728
- )
729
-
730
-
731
- @app.get(path="/api/v1/payload/{payload_id}/log", tags=["payload"])
732
- def download_payload_logfile(
733
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
734
- payload_id: int,
735
- ) -> FileResponse:
736
- """Download the logfile for a specific payload."""
737
-
738
- payload = payload_list.get_payload_item(index=payload_id)
739
-
740
- if payload is None:
741
- raise HTTPException(status_code=404, detail="Payload not found")
742
-
743
- filename = payload.logfile
744
-
745
- if not os.path.isfile(filename):
746
- raise HTTPException(
747
- status_code=404,
748
- detail="Log file -> '{}' not found".format(filename),
749
- )
750
- with open(filename, encoding="UTF-8") as file:
751
- content = file.read()
752
- return Response(
753
- content,
754
- media_type="application/octet-stream",
755
- headers={
756
- "Content-Disposition": f'attachment; filename="{os.path.basename(filename)}"',
757
- },
758
- )
759
-
760
-
761
- def get_cshost() -> str:
762
- """Get the cs_hostname from the environment Variable OTCS_PUBLIC_HOST otherwise read it from the otcs-frontend-configmap."""
763
-
764
- if "OTCS_PUBLIC_URL" in os.environ:
765
- return os.getenv("OTCS_PUBLIC_URL", "otcs")
766
-
767
- else:
768
- cm = app.k8s_object.get_config_map("otcs-frontend-configmap")
769
-
770
- if cm is None:
771
- raise HTTPException(
772
- status_code=500,
773
- detail=f"Could not read otcs-frontend-configmap from namespace: {app.k8s_object.get_namespace()}",
774
- )
775
-
776
- config_file = cm.data.get("config.yaml")
777
- config = yaml.safe_load(config_file)
778
-
779
- try:
780
- cs_url = HttpUrl(config.get("csurl"))
781
- except ValidationError as ve:
782
- raise HTTPException(
783
- status_code=500,
784
- detail="Could not read otcs_host from environment variable OTCS_PULIBC_URL or configmap otcs-frontend-configmap/config.yaml/cs_url",
785
- ) from ve
786
- return cs_url.host
787
-
788
-
789
- def __get_maintenance_mode_status() -> dict:
790
- """Get status of maintenance mode.
791
-
792
- Returns:
793
- dict:
794
- Details of maintenance mode.
795
-
796
- """
797
- ingress = app.k8s_object.get_ingress("otxecm-ingress")
798
-
799
- if ingress is None:
800
- raise HTTPException(
801
- status_code=500,
802
- detail="No ingress object found to read Maintenance Mode status",
803
- )
804
-
805
- enabled = False
806
- for rule in ingress.spec.rules:
807
- if rule.host == get_cshost():
808
- enabled = rule.http.paths[0].backend.service.name != "otcs-frontend"
809
-
810
- return {
811
- "enabled": enabled,
812
- "title": maint_settings.title,
813
- "text": maint_settings.text,
814
- "footer": maint_settings.footer,
815
- }
816
-
817
-
818
- @app.get(path="/api/v1/maintenance", tags=["maintenance"])
819
- async def get_maintenance_mode_status(
820
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # noqa: ARG001
821
- ) -> JSONResponse:
822
- """Return status of maintenance mode.
823
-
824
- Returns:
825
- dict:
826
- Details of maintenance mode.
827
-
828
- """
829
-
830
- return __get_maintenance_mode_status()
831
-
832
-
833
- def set_maintenance_mode_via_ingress(enabled: bool) -> None:
834
- """Set maintenance mode."""
835
-
836
- app.logger.warning(
837
- "Setting Maintenance Mode to -> %s",
838
- (enabled),
839
- )
840
-
841
- if enabled:
842
- app.k8s_object.update_ingress_backend_services(
843
- "otxecm-ingress",
844
- get_cshost(),
845
- "otxecm-customizer",
846
- 5555,
847
- )
848
- else:
849
- app.k8s_object.update_ingress_backend_services(
850
- "otxecm-ingress",
851
- get_cshost(),
852
- "otcs-frontend",
853
- 80,
854
- )
855
-
856
-
857
- @app.post(path="/api/v1/maintenance", tags=["maintenance"])
858
- async def set_maintenance_mode_options(
859
- user: Annotated[models.User, Depends(auth.get_authorized_user)], # pylint: disable=unused-argument # noqa: ARG001
860
- enabled: Annotated[bool, Form()],
861
- title: Annotated[str | None, Form()] = "",
862
- text: Annotated[str | None, Form()] = "",
863
- footer: Annotated[str | None, Form()] = "",
864
- ) -> dict:
865
- """Configure the Maintenance Mode and set options.
866
-
867
- Args:
868
- user (models.User):
869
- Added to enforce authentication requirement
870
- enabled (bool, optional):
871
- Enable or disable the maintenance mode to allow access to the OTCS Frontend.
872
- title (Optional[str], optional):
873
- Title for the Maintenance Page.
874
- text (Optional[str], optional):
875
- Text for the Maintenance Page.
876
- footer (Optional[str], optional):
877
- Text for the Footer of the Maintenance Page.
878
-
879
- Returns:
880
- dict: _description_
881
-
882
- """
883
- # Enable / Disable the acutual Maintenance Mode
884
- set_maintenance_mode_via_ingress(enabled)
885
-
886
- if title is not None and title != "":
887
- maint_settings.title = title
888
-
889
- if text is not None and text != "":
890
- maint_settings.text = text
891
-
892
- if footer is not None:
893
- maint_settings.footer = footer
894
-
895
- return __get_maintenance_mode_status()
896
-
897
-
898
- @app.get("/api/shutdown", include_in_schema=False)
899
- def shutdown(user: Annotated[models.User, Depends(auth.get_authorized_user)]) -> JSONResponse:
900
- """Endpoint to end the application."""
901
-
902
- app.logger.warning(
903
- "Shutting down the API - Requested via api by user -> %s",
904
- user.id,
905
- )
906
- os.kill(os.getpid(), signal.SIGTERM)
907
-
908
- return JSONResponse()
151
+ instrumentator.add(payload_logs_by_payload(PAYLOAD_LIST))
152
+ instrumentator.add(payload_logs_total(PAYLOAD_LIST))
909
153
 
910
154
 
911
155
  def run_api() -> None:
912
156
  """Start the FASTAPI Webserver."""
913
157
 
914
- uvicorn.run("pyxecm.customizer.api:app", host=api_settings.bind_address, port=api_settings.bind_port)
158
+ uvicorn.run(
159
+ "pyxecm.customizer.api:app",
160
+ host=api_settings.bind_address,
161
+ port=api_settings.bind_port,
162
+ workers=api_settings.workers,
163
+ )