pyxecm 2.0.0__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 (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 +568 -326
  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 +201 -37
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
  45. pyxecm-2.0.1.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.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.1.dist-info}/licenses/LICENSE +0 -0
  50. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
@@ -91,6 +91,16 @@ except ModuleNotFoundError:
91
91
  )
92
92
  pandas_installed = False
93
93
 
94
+ try:
95
+ import psycopg
96
+
97
+ psycopg_installed = True
98
+ except ModuleNotFoundError:
99
+ default_logger.warning(
100
+ "Module psycopg is not installed. Customizer will not support database command execution.",
101
+ )
102
+ psycopg_installed = False
103
+
94
104
  THREAD_NUMBER = 3
95
105
  BULK_THREAD_NUMBER = int(os.environ.get("BULK_THREAD_NUMBER", "1"))
96
106
  BULK_DOCUMENT_PATH = os.path.join(tempfile.gettempdir(), "bulkDocuments")
@@ -297,6 +307,17 @@ class Payload:
297
307
  _partitions = []
298
308
  _synchronized_partitions = []
299
309
 
310
+ """
311
+ _licenses: Lists of OTDS Licenses to be added to a resource.
312
+ Each element is a dict with these keys:
313
+ - enabled (bool, optional, default = True)
314
+ - path (str, mandatory)
315
+ - product_name (str, mandatory)
316
+ - resource (str, mandatory)
317
+ - description (str, optional)
318
+ """
319
+ _licenses = []
320
+
300
321
  """
301
322
  _oauth_clients: List of OTDS OAuth Clients. Each element
302
323
  is a dict with these keys:
@@ -419,7 +440,7 @@ class Payload:
419
440
  _admin_settings_post = []
420
441
 
421
442
  """
422
- exec_pod_commands: list of commands to be executed in the pods
443
+ _exec_pod_commands: List of commands to be executed in the pods.
423
444
  list elements need to be dicts with pod name, command, etc.
424
445
  - enabled (bool, optional, default = True)
425
446
  - command (str, mandatory)
@@ -430,14 +451,30 @@ class Payload:
430
451
  _exec_pod_commands = []
431
452
 
432
453
  """
433
- exec_pod_commands: list of commands to be executed
434
- list elements need to be dicts with pod name, command, etc.
454
+ _exec_commands: List of commands to be executed in the customizer pod (as local process).
455
+ Each list element need to be a dict with these keys:
435
456
  - enabled (bool, optional, default = True)
436
457
  - command (str, mandatory)
437
458
  - description (str, optional)
438
459
  """
439
460
  _exec_commands = []
440
461
 
462
+ """
463
+ _exec_database_commands: list of database command sets to be executed.
464
+ Each list is a dict with these keys:
465
+ - enabled (bool, optional, default = True)
466
+ - db_connection (dict, mandatory) - supported dictionary keys:
467
+ - db_name (str, mandatory) - name of the database
468
+ - db_hostname (str, mandatory) - hostname of the database server
469
+ - db_port (int, optional) - port to communicate to the database server; default is 5432
470
+ - db_username (str, mandatory) - username
471
+ - db_password (str, mandatory) - password
472
+ - db_commands (list, mandatory) - each list item is a dictionary with these keys:
473
+ - command (str, mandatory) - needs to have %s paceholder for each parameter
474
+ - params (list, optional) - parameter values to be inserted into the %s postions in the command
475
+ """
476
+ _exec_database_commands = []
477
+
441
478
  """
442
479
  external_systems (list): List of external systems.
443
480
  Each element is a dict with these keys:
@@ -706,20 +743,35 @@ class Payload:
706
743
  automated via the web user interface. Each element is a dict with these keys:
707
744
  - enabled (bool, optional, default = True)
708
745
  - name (str, mandatory)
746
+ - description (str, optional)
709
747
  - base_url (str, mandatory)
710
748
  - user_name (str, optional)
711
749
  - password (str, optional)
712
- - automations (list, mandatory)
713
- * type (str, optional, default = "")
714
- * page (str, optional, default = "")
715
- * elem (str, optional, default = "")
716
- * find (str, optional, default = "id")
717
- * value (str, optional, default = "")
718
- - wait-time (float, optional, default = 15.0) - wati time in seconds
750
+ - wait_time (float, optional, default = 15.0) - wait time in seconds
751
+ - wait_until (str, optional) - the page load / navigation `wait until` strategy. Possible values: `load`, `networkidle`, `domcontentloaded`
719
752
  - debug (bool, optional, default = False) - if True take screenshots and save to container
753
+ - automations (list, mandatory)
754
+ * type (str, optional, default = "") - possible types: `login`, `get_page`, `click_elem`, `set_elem`, `check_elem`
755
+ * page (str, optional, default = "") - the page-specific part of the URL. Will be concatenated with the `base_url`
756
+ * selector (str, optional, default = "")
757
+ * selector_type (str, optional, default = "id") - the find strategy - either `id`, `name`, `css`, `xpath`, `role`, `text`
758
+ * role_type (str, optional, default = "") - the ARIA role of an element. Only relevant for selector_type = `role`.
759
+ * value (str, optional, default = "") - the new value of element. Relevant for type = `set_elem`.
760
+ * user_field (str, optional, default = "") - the name of the HTML field holding the user name - only for type `login`.
761
+ * password_field (str, optional, default = "") - the name of the HTML field holding the password - only for type `login`.
762
+ * wait_until (str, optional, default = "") - an automation-step specific value for `wait_until` (see above)
763
+ * volume (int, optional, default = 141 (Enterprise Volume)) - the OTCS volume ID. Only relevant for type = `get_page`.
764
+ * path (list), optional, default = []) - a top-down list of folder / workspace names. Only relevant for type = `get_page`.
765
+ * navigation (bool, optional, default = False) - whether or not the click issues a nvigation event. Relevant only for type = `click_elem`.
766
+ * checkbox_state (bool, optional, default = None) - defines the required state of a checkbox element (True = checked, False = unchecked)
767
+ * attribute (str, optional, default = "") - the attribute name of an HTML element. Relevant only for type = `check_elem`.
768
+ * substring (bool, optional, default = False) - with or not a string comparison should consider substrings. Relevant only for type = `check_elem`.
769
+ * min_count (int, optional, default = 1) - defines how many elements should be found at a minimum by type = `check_elem`.
770
+ * want_exist (bool, optional, default = True) - defines for type = `check_elem` if the existence or non-existence should be checked.
720
771
  """
721
772
  _browser_automations = []
722
773
  _browser_automations_post = []
774
+ _test_automations = []
723
775
 
724
776
  """
725
777
  _security_clearances: List of Security Clearances. Each element is a dict with these keys:
@@ -1149,11 +1201,11 @@ class Payload:
1149
1201
 
1150
1202
  _transport_extractions: list = []
1151
1203
  _transport_replacements: list = []
1152
- _otawpsection = []
1204
+ _appworks_configurations = []
1153
1205
 
1154
1206
  _avts_repositories: list = []
1155
1207
 
1156
- _feme: list = []
1208
+ _embeddings: list = []
1157
1209
 
1158
1210
  # Disable Status files
1159
1211
  upload_status_files: bool = True
@@ -1175,6 +1227,7 @@ class Payload:
1175
1227
  browser_automation_object: BrowserAutomation | None,
1176
1228
  placeholder_values: dict,
1177
1229
  log_header_callback: Callable,
1230
+ browser_headless: bool = True,
1178
1231
  stop_on_error: bool = False,
1179
1232
  aviator_enabled: bool = False,
1180
1233
  upload_status_files: bool = True,
@@ -1219,6 +1272,8 @@ class Payload:
1219
1272
  A dictionary of placeholder values to be replaced in admin settings.
1220
1273
  log_header_callback:
1221
1274
  Method to print a section break / header line into the log.
1275
+ browser_headless (bool):
1276
+ If true, the Browser for the Automation will be started in Headless mode (default)
1222
1277
  stop_on_error (bool):
1223
1278
  This flag controls if transport deployment should stop
1224
1279
  if a transport deployment in OTCS fails.
@@ -1263,6 +1318,7 @@ class Payload:
1263
1318
  self._nhc = None # National Hurricane Center
1264
1319
  self._avts = avts_object
1265
1320
  self._browser_automation = browser_automation_object
1321
+ self._browser_headless = browser_headless
1266
1322
  self._custom_settings_dir = custom_settings_dir
1267
1323
  self._placeholder_values = placeholder_values
1268
1324
  self._otcs_restart_callback = otcs_restart_callback
@@ -1372,6 +1428,7 @@ class Payload:
1372
1428
  self._webhooks_post = self.get_payload_section("webHooksPost")
1373
1429
  self._resources = self.get_payload_section("resources")
1374
1430
  self._partitions = self.get_payload_section("partitions")
1431
+ self._licenses = self.get_payload_section("licenses")
1375
1432
  self._synchronized_partitions = self.get_payload_section(
1376
1433
  "synchronizedPartitions",
1377
1434
  )
@@ -1390,6 +1447,8 @@ class Payload:
1390
1447
  self._admin_settings_post = self.get_payload_section("adminSettingsPost")
1391
1448
  self._exec_pod_commands = self.get_payload_section("execPodCommands")
1392
1449
  self._exec_commands = self.get_payload_section("execCommands")
1450
+ self._kubernetes = self.get_payload_section("kubernetes")
1451
+ self._exec_database_commands = self.get_payload_section("execDatabaseCommands")
1393
1452
  self._external_systems = self.get_payload_section("externalSystems")
1394
1453
  self._transport_packages = self.get_payload_section("transportPackages")
1395
1454
  self._content_transport_packages = self.get_payload_section(
@@ -1440,9 +1499,11 @@ class Payload:
1440
1499
  self._browser_automations_post = self.get_payload_section(
1441
1500
  "browserAutomationsPost",
1442
1501
  )
1443
- self._otawpsection = self.get_payload_section("platformCustomConfig")
1502
+ self._test_automations = self.get_payload_section("testAutomations")
1503
+ self._appworks_configurations = self.get_payload_section("appworks")
1444
1504
  self._avts_repositories = self.get_payload_section("avtsRepositories")
1445
- self._feme = self.get_payload_section("feme")
1505
+ self._avts_questions = self.get_payload_section("avtsQuestions")
1506
+ self._embeddings = self.get_payload_section("embeddings")
1446
1507
 
1447
1508
  return self._payload
1448
1509
 
@@ -1526,319 +1587,695 @@ class Payload:
1526
1587
 
1527
1588
  # end method definition
1528
1589
 
1529
- def ot_awp_create_project(self) -> bool:
1530
- """Initiate the configuration of AppWorks projects.
1590
+ def process_appworks_configurations(self, section_name: str = "appworks") -> bool:
1591
+ """Process the configurations for AppWorks projects.
1531
1592
 
1532
1593
  This method is responsible for setting up the necessary configurations for AppWorks projects.
1533
- If the payload contains a `platformCustomConfig` section, it will execute the corresponding actions
1594
+ If the payload contains a `appworks` section, it will execute the corresponding actions
1534
1595
  to process and apply the custom configuration.
1535
1596
 
1597
+ Args:
1598
+ section_name (str, optional):
1599
+ The name of the payload section. It can be overridden
1600
+ for cases where multiple sections of same type
1601
+ are used (e.g. the "Post" sections).
1602
+ This name is also used for the "success" status
1603
+ files written to the Admin Personal Workspace.
1604
+
1536
1605
  Returns:
1537
- bool: `True` on success, `False` on failure.
1606
+ bool:
1607
+ True, if payload has been processed without errors, False otherwise.
1538
1608
 
1539
1609
  """
1540
1610
 
1541
- for value in self._otawpsection.values():
1542
- if value.get("resourceConfig"):
1543
- resource_name = value["organization"]
1544
- access_role_name = "Access to " + resource_name
1545
- awp_resource = self._otds.get_resource(resource_name)
1611
+ if not self._appworks_configurations:
1612
+ self.logger.info(
1613
+ "Payload section -> '%s' is empty. Skipping...",
1614
+ section_name,
1615
+ )
1616
+ return True
1546
1617
 
1547
- if not awp_resource:
1548
- self.logger.info(
1549
- "OTDS resource -> '%s' for AppWorks Platform does not yet exist. Creating...",
1550
- resource_name,
1551
- )
1552
- awp_resource = self._otds.add_resource(
1553
- name=resource_name,
1554
- description="AppWorks Platform",
1555
- display_name="AppWorks Platform",
1556
- additional_payload=self.appworks_resource_payload(
1557
- resource_name,
1558
- self._otawp.username(),
1559
- self._otawp.password(),
1560
- ),
1561
- )
1562
- else:
1563
- self.logger.info(
1564
- "OTDS resource -> '%s' for AppWorks Platform does already exist.",
1565
- resource_name,
1566
- )
1618
+ # If this payload section has been processed successfully before we
1619
+ # can return True and skip processing it once more:
1620
+ if self.check_status_file(payload_section_name=section_name):
1621
+ return True
1567
1622
 
1568
- awp_resource_id = awp_resource["resourceID"]
1623
+ success: bool = True
1569
1624
 
1570
- # Loop to wait for OTCS to create its OTDS user partition:
1571
- otcs_partition = self._otds.get_partition(
1572
- self._otawp.otcs_partition_name(),
1573
- show_error=False,
1625
+ #
1626
+ # 1: Loop to create the required OTDS resources for the AppWorks organization:
1627
+ #
1628
+ for appworks_configuration in self._appworks_configurations:
1629
+ organization = appworks_configuration.get("organization", None)
1630
+ if not organization:
1631
+ self.logger.error("AppWorks configuration is missing the organization name! Skipping...")
1632
+ success = False
1633
+ continue
1634
+
1635
+ self._log_header_callback(
1636
+ text="Process AppWorks resource configuration for '{}'".format(organization), char="-"
1637
+ )
1638
+
1639
+ # Check if user has been explicitly disabled in payload
1640
+ # (enabled = false). In this case we skip the element:
1641
+ if not appworks_configuration.get("enabled", True):
1642
+ self.logger.info(
1643
+ "Payload for AppWorks configuration -> '%s' is disabled. Skipping...",
1644
+ organization,
1574
1645
  )
1575
- while otcs_partition is None:
1576
- self.logger.warning(
1577
- "OTDS user partition for Content Server with name -> '%s' does not exist yet. Waiting...",
1578
- self._otawp.otcs_partition_name(),
1579
- )
1646
+ continue
1580
1647
 
1581
- time.sleep(30)
1582
- otcs_partition = self._otds.get_partition(
1583
- self._otawp.otcs_partition_name(),
1584
- show_error=False,
1585
- )
1648
+ if not appworks_configuration.get("resource_config", False):
1649
+ self.logger.info("AppWorks resource configuration is disabled for -> '%s'. Skipping...", organization)
1650
+ continue
1586
1651
 
1587
- # Add the OTDS user partition for OTCS to the AppWorks Platform Access Role in OTDS.
1588
- # This will effectvely sync all OTCS users with AppWorks Platform:
1589
- self._otds.add_partition_to_access_role(
1590
- access_role_name,
1591
- self._otawp.otcs_partition_name(),
1652
+ access_role_name = "Access to " + organization
1653
+
1654
+ # make sure code is idempotent and only try to add ressource if it doesn't exist already:
1655
+ awp_resource = self._otds.get_resource(organization)
1656
+ if not awp_resource:
1657
+ self.logger.info(
1658
+ "OTDS resource -> '%s' for AppWorks Platform does not yet exist. Creating...",
1659
+ organization,
1660
+ )
1661
+ awp_resource = self._otds.add_resource(
1662
+ name=organization,
1663
+ description="AppWorks Platform - {}".format(organization),
1664
+ display_name="AppWorks Platform - {}".format(organization),
1665
+ additional_payload=OTAWP.resource_payload(
1666
+ org_name=organization,
1667
+ username=self._otawp.username(),
1668
+ password=self._otawp.password(),
1669
+ ),
1670
+ )
1671
+ else:
1672
+ self.logger.info(
1673
+ "OTDS resource -> '%s' for AppWorks Platform does already exist.",
1674
+ organization,
1592
1675
  )
1593
1676
 
1594
- # Add the OTDS admin partition to the AppWorks Platform Access Role in OTDS.
1595
- self._otds.add_partition_to_access_role(
1596
- access_role_name,
1597
- self._otawp.otds_admin_partition_mame(),
1677
+ awp_resource_id = awp_resource["resourceID"]
1678
+
1679
+ self.logger.info(
1680
+ "AppWorks Platform organization -> '%s' got OTDS resource ID -> %s",
1681
+ organization,
1682
+ awp_resource_id,
1683
+ )
1684
+
1685
+ # Loop to wait for OTCS to create its OTDS user partition:
1686
+ otcs_partition = self._otds.get_partition(
1687
+ self._otcs.partition_name(),
1688
+ show_error=False,
1689
+ )
1690
+ while otcs_partition is None:
1691
+ self.logger.warning(
1692
+ "OTDS user partition -> '%s' for Content Server does not exist yet. Waiting...",
1693
+ self._otcs.partition_name(),
1598
1694
  )
1599
1695
 
1600
- # Set Group inclusion for Access Role for OTAWP to "True":
1601
- self._otds.update_access_role_attributes(
1602
- access_role_name,
1603
- [{"name": "pushAllGroups", "values": ["True"]}],
1696
+ time.sleep(30)
1697
+ otcs_partition = self._otds.get_partition(
1698
+ self._otcs.partition_name(),
1699
+ show_error=False,
1604
1700
  )
1605
1701
 
1606
- # Add ResourceID User to OTDSAdmin to allow push
1607
- self._otds.add_user_to_group(
1608
- user=str(awp_resource_id) + "@otds.admin",
1609
- group="otdsadmins@otds.admin",
1702
+ # Add the OTDS user partition for OTCS to the AppWorks Platform Access Role in OTDS.
1703
+ # This will effectvely sync all OTCS users with AppWorks Platform:
1704
+ self._otds.add_partition_to_access_role(
1705
+ access_role=access_role_name,
1706
+ partition=self._otcs.partition_name(),
1707
+ )
1708
+
1709
+ # Add the OTDS admin partition to the AppWorks Platform Access Role in OTDS.
1710
+ self._otds.add_partition_to_access_role(
1711
+ access_role=access_role_name,
1712
+ partition=self._otds.admin_partition_name(),
1713
+ )
1714
+
1715
+ # Set Group inclusion for Access Role for OTAWP to "True":
1716
+ self._otds.update_access_role_attributes(
1717
+ name=access_role_name,
1718
+ attribute_list=[{"name": "pushAllGroups", "values": ["True"]}],
1719
+ )
1720
+
1721
+ # Add ResourceID User to 'otdsadmins' group to allow push
1722
+ self._otds.add_user_to_group(
1723
+ user=str(awp_resource_id) + "@otds.admin",
1724
+ group="otdsadmins@otds.admin",
1725
+ )
1726
+
1727
+ # Allow impersonation for all users:
1728
+ self._otds.impersonate_resource(organization)
1729
+
1730
+ # Editing configmap
1731
+ config_map = self._k8s.get_config_map(config_map_name=self._otawp.config_map_name())
1732
+ if not config_map:
1733
+ self.logger.error(
1734
+ "Failed to retrieve AppWorks Kubernetes config map -> '%s'",
1735
+ self._otawp.config_map_name(),
1736
+ )
1737
+ success = False
1738
+ else:
1739
+ self.logger.info(
1740
+ "Update Kubernetes config map for AppWorks organization -> '%s' with OTDS resource IDs...",
1741
+ organization,
1742
+ )
1743
+ solution = yaml.safe_load(
1744
+ config_map.data[organization + ".yaml"],
1610
1745
  )
1611
1746
 
1612
- # Allow impersonation for all users:
1613
- self._otds.impersonate_resource(resource_name)
1747
+ solution["platform"]["organizations"][organization]["otds"]["resourceId"] = awp_resource_id
1748
+ solution["platform"]["organizations"][organization]["database"]["connections"]["sysConnection"][
1749
+ "connectionString"
1750
+ ] = "jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}"
1614
1751
 
1615
- # Editing configmap
1616
- config_map = self._k8s.get_config_map(config_map_name=self._otawp.config_map_name())
1617
- if not config_map:
1618
- self.logger.error(
1619
- "Failed to retrieve AppWorks Kubernetes Config Map -> %s",
1620
- self._otawp.config_map_name(),
1752
+ solution["platform"]["organizations"][organization]["database"]["connections"]["sysConnection"][
1753
+ "database"
1754
+ ] = "${DATABASE_NAME}"
1755
+
1756
+ solution["platform"]["organizations"][organization]["database"]["connections"]["sysConnection"][
1757
+ "password"
1758
+ ] = "${DATABASE_PASSWORD}"
1759
+
1760
+ solution["platform"]["organizations"][organization]["content"]["ContentServer"]["contentServerUrl"] = (
1761
+ self._otcs.cs_url()
1762
+ )
1763
+ solution["platform"]["organizations"][organization]["content"]["ContentServer"][
1764
+ "contentServerSupportDirectoryUrl"
1765
+ ] = self._otcs.cs_support_url()
1766
+ solution["platform"]["organizations"][organization]["content"]["ContentServer"]["otdsResourceId"] = (
1767
+ self._otcs.resource_id()
1768
+ )
1769
+ solution["platform"]["organizations"][organization]["authenticators"]["OTDSAuth"]["publicLoginUrl"] = (
1770
+ self._otds.base_url() + "/otdsws/login"
1771
+ )
1772
+
1773
+ config_map.data[organization + ".yaml"] = yaml.dump(solution)
1774
+ result = self._k8s.replace_config_map(
1775
+ config_map_name=self._otawp.config_map_name(),
1776
+ config_map_data=config_map.data,
1777
+ )
1778
+ if result:
1779
+ self.logger.info(
1780
+ "Successfully updated AppWorks solution YAML for organization -> '%s'.",
1781
+ organization,
1621
1782
  )
1622
1783
  else:
1623
- self.logger.info(
1624
- "Update '%s' Kubernetes Config Map with OTDS resource IDs...",
1625
- resource_name,
1626
- )
1627
- solution = yaml.safe_load(
1628
- config_map.data[resource_name + ".yaml"],
1629
- )
1630
-
1631
- solution["platform"]["organizations"][resource_name]["otds"]["resourceId"] = awp_resource_id
1632
-
1633
- solution["platform"]["organizations"][resource_name]["database"]["connections"]["sysConnection"][
1634
- "connectionString"
1635
- ] = "jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}"
1636
-
1637
- solution["platform"]["organizations"][resource_name]["database"]["connections"]["sysConnection"][
1638
- "database"
1639
- ] = "${DATABASE_NAME}"
1640
-
1641
- solution["platform"]["organizations"][resource_name]["database"]["connections"]["sysConnection"][
1642
- "password"
1643
- ] = "${DATABASE_PASSWORD}"
1644
-
1645
- solution["platform"]["organizations"][resource_name]["content"]["ContentServer"][
1646
- "contentServerUrl"
1647
- ] = f"{self._otawp.otcs_url()!s}{self._otawp.otcs_base_path()}"
1648
- solution["platform"]["organizations"][resource_name]["content"]["ContentServer"][
1649
- "contentServerSupportDirectoryUrl"
1650
- ] = f"{self._otawp.otcs_url()!s}/cssupport"
1651
- solution["platform"]["organizations"][resource_name]["content"]["ContentServer"][
1652
- "otdsResourceId"
1653
- ] = self._otawp.otcs_resource_id()
1654
- solution["platform"]["organizations"][resource_name]["authenticators"]["OTDSAuth"][
1655
- "publicLoginUrl"
1656
- ] = f"{self._otawp.otds_url()!s}" + "/otdsws/login"
1657
-
1658
- config_map.data[resource_name + ".yaml"] = yaml.dump(solution)
1659
- result = self._k8s.replace_config_map(
1660
- config_map_name=self._otawp.config_map_name(),
1661
- config_map_data=config_map.data,
1662
- )
1663
- if result:
1664
- self.logger.info(
1665
- "Successfully updated '%s' Solution YAML.",
1666
- resource_name,
1667
- )
1668
- else:
1669
- self.logger.error(
1670
- "Failed to update '%s' Solution YAML.",
1671
- resource_name,
1672
- )
1673
- self.logger.debug(
1674
- "Solution YAML for '%s' -> %s",
1675
- resource_name,
1676
- solution,
1677
- )
1678
- # Add SPS license for OTAWP
1679
- license_name = self._otawp.product_name()
1680
- product_name = self._otawp.product_name() + "_" + resource_name.upper()
1681
- product_description = self._otawp.product_name() + resource_name
1682
- if os.path.isfile(self._otawp.license_file()):
1683
- self.logger.info(
1684
- "Found OTAWP license file -> '%s', assiging it to ressource -> '%s'...",
1685
- self._otawp.license_file(),
1686
- resource_name,
1784
+ self.logger.error(
1785
+ "Failed to update AppWorks solution YAML for organization -> '%s'!",
1786
+ organization,
1687
1787
  )
1788
+ self.logger.debug(
1789
+ "Solution YAML for Appworks organization -> '%s': %s",
1790
+ organization,
1791
+ solution,
1792
+ )
1793
+ # Add SPS license for OTAWP
1794
+ license_name = self._otawp.product_name()
1795
+ product_name = self._otawp.product_name() + "_" + organization.upper()
1796
+ product_description = self._otawp.product_name() + organization
1797
+ if os.path.isfile(self._otawp.license_file()):
1798
+ self.logger.info(
1799
+ "Found OTAWP license file -> '%s', assiging it to OTDS resource -> '%s'...",
1800
+ self._otawp.license_file(),
1801
+ organization,
1802
+ )
1688
1803
 
1689
- otawp_license = self._otds.add_license_to_resource(
1804
+ otawp_license = self._otds.add_license_to_resource(
1805
+ path_to_license_file=self._otawp.license_file(),
1806
+ product_name=product_name,
1807
+ product_description=product_description,
1808
+ resource_id=awp_resource["resourceID"],
1809
+ )
1810
+ if not otawp_license:
1811
+ self.logger.error(
1812
+ "Couldn't apply license -> '%s' for product -> '%s' to OTDS resource -> '%s'",
1690
1813
  self._otawp.license_file(),
1691
1814
  product_name,
1692
- product_description,
1693
1815
  awp_resource["resourceID"],
1694
1816
  )
1695
- if not otawp_license:
1696
- self.logger.error(
1697
- "Couldn't apply license -> '%s' for product -> '%s' to OTDS resource -> '%s'",
1698
- self._otawp.license_file(),
1699
- product_name,
1700
- awp_resource["resourceID"],
1701
- )
1702
- else:
1817
+ else:
1818
+ self.logger.info(
1819
+ "Successfully applied license -> '%s' for product -> '%s' to OTDS resource -> '%s'",
1820
+ self._otawp.license_file(),
1821
+ product_name,
1822
+ awp_resource["resourceID"],
1823
+ )
1824
+
1825
+ # Assign AppWorks license to Content Server Members Partiton and otds.admin:
1826
+ for partition_name in ["otds.admin", self._otcs.partition_name()]:
1827
+ if self._otds.is_partition_licensed(
1828
+ partition_name=partition_name,
1829
+ resource_id=awp_resource["resourceID"],
1830
+ license_feature="USERS",
1831
+ license_name=license_name,
1832
+ ):
1703
1833
  self.logger.info(
1704
- "Successfully applied license -> '%s' for product -> '%s' to OTDS resource -> '%s'",
1705
- self._otawp.license_file(),
1834
+ "Partition -> '%s' is already licensed for -> '%s' (%s)",
1835
+ partition_name,
1706
1836
  product_name,
1707
- awp_resource["resourceID"],
1837
+ "USERS",
1708
1838
  )
1709
-
1710
- # Assign AppWorks license to Content Server Members Partiton and otds.admin:
1711
- for partition_name in ["otds.admin", self._otawp.otcs_partition_name()]:
1712
- if self._otds.is_partition_licensed(
1839
+ else:
1840
+ assigned_license = self._otds.assign_partition_to_license(
1713
1841
  partition_name=partition_name,
1714
1842
  resource_id=awp_resource["resourceID"],
1715
1843
  license_feature="USERS",
1716
1844
  license_name=license_name,
1717
- ):
1718
- self.logger.info(
1719
- "Partition -> '%s' is already licensed for -> '%s' (%s)",
1845
+ )
1846
+ if not assigned_license:
1847
+ self.logger.error(
1848
+ "Partition -> '%s' could not be assigned to license -> '%s' (%s)",
1720
1849
  partition_name,
1721
1850
  product_name,
1722
1851
  "USERS",
1723
1852
  )
1853
+ success = False
1724
1854
  else:
1725
- assigned_license = self._otds.assign_partition_to_license(
1855
+ self.logger.info(
1856
+ "Partition -> '%s' successfully assigned to license -> '%s' (%s)",
1726
1857
  partition_name,
1727
- awp_resource["resourceID"],
1858
+ product_name,
1728
1859
  "USERS",
1729
- license_name,
1730
1860
  )
1731
- if not assigned_license:
1732
- self.logger.error(
1733
- "Partition -> '%s' could not be assigned to license -> '%s' (%s)",
1734
- partition_name,
1735
- product_name,
1736
- "USERS",
1737
- )
1738
- else:
1739
- self.logger.info(
1740
- "Partition -> '%s' successfully assigned to license -> '%s' (%s)",
1741
- partition_name,
1742
- product_name,
1743
- "USERS",
1744
- )
1861
+ # end for partition_name in ["otds.admin", self._otcs.partition_name()]:
1862
+ # end if os.path.isfile(self._otawp.license_file()):
1863
+
1864
+ self.logger.info("Restart AppWorks Kubernetes stateful set -> '%s'", self._otawp.hostname())
1865
+
1866
+ self._k8s.restart_stateful_set(sts_name=self._otawp.hostname(), force=True, wait=True)
1867
+
1868
+ self._otawp.set_organization(organization)
1869
+ otawp_cookie = self._otawp.authenticate(revalidate=True)
1870
+ if not otawp_cookie:
1871
+ self.logger.error(
1872
+ "Authentication at AppWorks failed. Cannot proceed with processing of AppWorks configuration -> '%s'",
1873
+ organization,
1874
+ )
1875
+ success = False
1876
+ continue
1877
+ self._otawp.create_cws_config(
1878
+ partition=self._otcs.partition_name(), resource_name=organization, otcs_url=self._otcs.cs_url()
1879
+ )
1880
+ self._otawp.assign_role_to_user(
1881
+ organization=organization, user_name=self._otawp.username(), role_name="Developer"
1882
+ )
1883
+ # end for appworks_configuration in self._appworks_configurations:
1884
+
1885
+ #
1886
+ # 2: Loop to create the AppWorks workspaces, projects, and entities:
1887
+ #
1888
+ for appworks_configuration in self._appworks_configurations:
1889
+ organization = appworks_configuration.get("organization", None)
1890
+ if not organization:
1891
+ self.logger.error("AppWorks configuration is missing the organization name! Skipping...")
1892
+ success = False
1893
+ continue
1745
1894
 
1746
- self.logger.info("Scale AppWorks Kubernetes Stateful Set to 0...")
1747
- self._k8s.scale_stateful_set(
1748
- sts_name=self._otawp.hostname(),
1749
- scale=0,
1895
+ # Check if user has been explicitly disabled in payload
1896
+ # (enabled = false). In this case we skip the element:
1897
+ if not appworks_configuration.get("enabled", True):
1898
+ self.logger.info(
1899
+ "Payload for AppWorks configuration -> '%s' is disabled. Skipping...",
1900
+ organization,
1750
1901
  )
1902
+ continue
1751
1903
 
1752
- is_pod_deleted = self._k8s.verify_pod_deleted(pod_name="appworks-0")
1753
- if is_pod_deleted is False:
1754
- return False
1904
+ self._log_header_callback(
1905
+ text="Process AppWorks workspaces, project, entities for '{}'".format(organization), char="-"
1906
+ )
1755
1907
 
1756
- self.logger.info("Scale AppWorks Kubernetes Stateful Set to 1...")
1757
- self._k8s.scale_stateful_set(
1758
- sts_name=self._otawp.hostname(),
1759
- scale=1,
1908
+ self._otawp.set_organization(organization)
1909
+ otawp_cookie = self._otawp.authenticate(revalidate=True)
1910
+ if not otawp_cookie:
1911
+ self.logger.error(
1912
+ "Authentication at AppWorks failed. Cannot proceed with processing of AppWorks configutation -> '%s'!",
1913
+ organization,
1760
1914
  )
1915
+ success = False
1916
+ continue
1761
1917
 
1762
- ispod_running = self._k8s.verify_pod_status(pod_name="appworks-0")
1763
- if ispod_running is False:
1764
- return False
1918
+ if "workspaces" not in appworks_configuration:
1919
+ self.logger.warning(
1920
+ "Missing workspace information in AppWorks configuration -> '%s'. Skipping...", organization
1921
+ )
1922
+ continue
1765
1923
 
1766
- self._otawp.authenticate(revalidate=False)
1767
- self._otawp.set_organization(resource_name)
1768
- self._otawp.create_cws_config(self._otawp.otcs_partition_name(), resource_name, self._otawp.otcs_url())
1769
- self._otawp.assign_developer_role_to_user(resource_name, self._otawp.username(), "Developer")
1770
-
1771
- for value in self._otawpsection.values():
1772
- if "cws" in value and "workspaces" in value["cws"]:
1773
- for workspace in value["cws"]["workspaces"]:
1774
- workspace_gui_id = workspace["workspaceGuiID"]
1775
- workspace_name = workspace["name"]
1776
- workspace_path = workspace["path"]
1777
- organization = value["organization"]
1778
- self._otawp.set_organization(organization)
1779
- if workspace_gui_id and workspace_name and workspace_path:
1780
- ispod_running = self._k8s.verify_pod_status(
1781
- pod_name="appworks-0",
1782
- )
1783
- if ispod_running is False:
1784
- return False
1785
- self._otawp.authenticate(revalidate=False)
1786
- respose = self._otawp.create_workspace_with_retry(
1787
- workspace_name=workspace_name,
1788
- workspace_gui_id=workspace_gui_id,
1789
- )
1790
- if not self._otawp.validate_workspace_response(
1791
- respose,
1792
- workspace_name,
1793
- ):
1794
- return False
1795
- if not self._otawp.is_workspace_already_exists(
1796
- respose,
1797
- workspace_name,
1798
- ):
1799
- self._otawp.sync_workspace(
1924
+ for workspace in appworks_configuration["workspaces"]:
1925
+ workspace_id = workspace.get("workspace_id", None)
1926
+ workspace_name = workspace.get("name", None)
1927
+ workspace_path = workspace.get("path", None)
1928
+ if not workspace_id or not workspace_name or not workspace_path:
1929
+ self.logger.error(
1930
+ "AppWorks workspace configuration for -> '%s'%s requires 'workspace_id', 'name', and 'path' settings. Skipping...",
1931
+ organization,
1932
+ " (workspace name -> {})".format(workspace_name) if workspace_name else "",
1933
+ )
1934
+ success = False
1935
+ continue
1936
+
1937
+ response, created = self._otawp.create_workspace(
1938
+ workspace_name=workspace_name, workspace_id=workspace_id
1939
+ )
1940
+ if not response:
1941
+ self.logger.info("Failed to create workspace -> '%s' (%s)", workspace_name, workspace_id)
1942
+ success = False
1943
+ continue
1944
+
1945
+ if created:
1946
+ self.logger.info("Setup new workspace -> '%s' (%s)...", workspace_name, workspace_id)
1947
+ response = self._otawp.sync_workspace(
1948
+ workspace_name=workspace_name,
1949
+ workspace_id=workspace_id,
1950
+ )
1951
+ if not response:
1952
+ self.logger.error("Failed to synchronize workspace -> '%s' (%s)", workspace_name, workspace_id)
1953
+ success = False
1954
+ continue
1955
+ self.logger.info(
1956
+ "Copy projects artifacts to workspace -> '%s' (%s) in AppWorks pod -> '%s'...",
1957
+ workspace_name,
1958
+ workspace_id,
1959
+ "appworks-0",
1960
+ )
1961
+ self._k8s.exec_pod_command(
1962
+ pod_name="appworks-0",
1963
+ command=[
1964
+ "/bin/sh",
1965
+ "-c",
1966
+ f'cp -r "{workspace_path}/"* "/opt/appworks/cluster/shared/cws/sync/{organization}/{workspace_name}"',
1967
+ ],
1968
+ timeout=600,
1969
+ )
1970
+ self.logger.info(
1971
+ "Copying of projects artifacts to workspace -> '%s' (%s) completed.",
1972
+ workspace_name,
1973
+ workspace_id,
1974
+ )
1975
+ self.logger.info("Re-sync existing workspace -> '%s' (%s)...", workspace_name, workspace_id)
1976
+ response = self._otawp.sync_workspace(
1977
+ workspace_name=workspace_name,
1978
+ workspace_id=workspace_id,
1979
+ )
1980
+ if not response:
1981
+ self.logger.error("Failed to synchronize workspace -> '%s' (%s)", workspace_name, workspace_id)
1982
+ success = False
1983
+ continue
1984
+
1985
+ if "projects" in workspace:
1986
+ for project in workspace["projects"]:
1987
+ if project.get("name") and project.get("documentId"):
1988
+ if not self._otawp.publish_project(
1800
1989
  workspace_name=workspace_name,
1801
- workspace_id=workspace_gui_id,
1802
- )
1803
- self.logger.info(
1804
- "Start copying of projects to workspace...",
1990
+ workspace_id=workspace_id,
1991
+ project_name=project.get("name"),
1992
+ project_id=project.get("documentId"),
1993
+ ):
1994
+ success = False
1995
+ continue
1996
+ else:
1997
+ self.logger.error(
1998
+ "Skipping project -> '%s' due to missing required project fields 'name' or 'documentId'.",
1999
+ project.get("name"),
1805
2000
  )
1806
- self._k8s.exec_pod_command(
1807
- pod_name="appworks-0",
1808
- command=[
1809
- "/bin/sh",
1810
- "-c",
1811
- f'cp -r "{workspace_path}/"* "/opt/appworks/cluster/shared/cws/sync/system/{workspace_name}"',
1812
- ],
2001
+ success = False
2002
+ continue
2003
+ # end for project in workspace["projects"]:
2004
+ # end if "projects" in workspace
2005
+ # end for workspace in value["cws"]["workspaces"]
2006
+
2007
+ # Process the entities in the payload:
2008
+ entities = appworks_configuration.get("entities", [])
2009
+ if entities:
2010
+ self._log_header_callback(
2011
+ text="Process AppWorks entities for organization -> '{}'".format(organization), char="-"
2012
+ )
2013
+ for entity in entities:
2014
+ if not self.process_appworks_entity(entity=entity):
2015
+ success = False
2016
+ self.logger.info("Entity processing completed for organization -> '%s'.", organization)
2017
+ # end for appworks_configuration in self._appworks_configurations:
2018
+
2019
+ self.write_status_file(
2020
+ success=success,
2021
+ payload_section_name=section_name,
2022
+ payload_section=self._appworks_configurations,
2023
+ )
2024
+
2025
+ return success
2026
+
2027
+ # end method definition
2028
+
2029
+ def process_appworks_entity(self, entity: dict) -> bool:
2030
+ """Process a single AppWorks entity payload.
2031
+
2032
+ Args:
2033
+ entity (dict):
2034
+ Entity payload.
2035
+ Should have a selection of the following keys:
2036
+ * type
2037
+ * name
2038
+ * description
2039
+ * status
2040
+ * prefix
2041
+ * category
2042
+ * priority
2043
+ * customer
2044
+ * case_type
2045
+ * ...
2046
+
2047
+ Returns:
2048
+ bool: True = success, False = failure.
2049
+
2050
+ """
2051
+
2052
+ entity_type = entity.get("type")
2053
+ if not entity_type:
2054
+ return False
2055
+
2056
+ match entity_type:
2057
+ case "category":
2058
+ cat = self._otawp.get_category_by_name(name=entity.get("name"))
2059
+ if cat:
2060
+ cat_id = self._otawp.get_entity_value(entity=cat, key="id")
2061
+ self.logger.info(
2062
+ "Category -> '%s' (%s) does already exist. Skipping...", entity.get("name"), str(cat_id)
2063
+ )
2064
+ self.logger.debug("Category -> %s", str(cat))
2065
+ else:
2066
+ response = self._otawp.create_category(
2067
+ case_prefix=entity.get("prefix"),
2068
+ name=entity.get("name"),
2069
+ description=entity.get("description", ""),
2070
+ status=entity.get("status", 1),
2071
+ )
2072
+ if not response or "Identity" not in response:
2073
+ self.logger.error("Failed to create category -> '%s'!", entity.get("name"))
2074
+ return False
2075
+ cat_id = response["Identity"].get("Id")
2076
+ self.logger.info(
2077
+ "Successfully created category -> '%s' (%s).",
2078
+ entity.get("name"),
2079
+ cat_id,
2080
+ )
2081
+ self.logger.debug("Response -> %s", str(response))
2082
+ if "sub_entities" in entity:
2083
+ for sub_entity in entity["sub_entities"]:
2084
+ if sub_entity["type"] != "subCategory":
2085
+ self.logger.warning(
2086
+ "Found a category sub-entities with wrong type -> '%s'", sub_entity["type"]
1813
2087
  )
1814
- self.logger.info("Copying of projects to workspace completed.")
1815
- self._otawp.sync_workspace(
1816
- workspace_name=workspace_name,
1817
- workspace_id=workspace_gui_id,
2088
+ continue
2089
+ response = self._otawp.create_sub_category(
2090
+ parent_id=cat_id,
2091
+ name=sub_entity.get("name"),
2092
+ description=sub_entity.get("description", ""),
2093
+ status=sub_entity.get("status", 1),
2094
+ )
2095
+ if not response or "Identity" not in response:
2096
+ self.logger.error("Failed to create sub-category -> '%s'!", sub_entity.get("name"))
2097
+ return False
2098
+ self.logger.info(
2099
+ "Successfully created sub-category -> '%s' (%s) in parent category -> '%s' (%s).",
2100
+ sub_entity.get("name"),
2101
+ response["Identity"].get("Id"),
2102
+ entity.get("name"),
2103
+ cat_id,
2104
+ )
2105
+ self.logger.debug("Response -> %s", str(response))
2106
+ return True
2107
+ case "priority":
2108
+ priority = self._otawp.get_priority_by_name(name=entity.get("name"))
2109
+ if priority:
2110
+ priority_id = self._otawp.get_entity_value(entity=priority, key="id")
2111
+ (
2112
+ self.logger.info(
2113
+ "Priority -> '%s' (%s) does already exist. Skipping...",
2114
+ entity.get("name"),
2115
+ str(priority_id),
1818
2116
  )
2117
+ )
2118
+ return True
2119
+ response = self._otawp.create_priority(
2120
+ name=entity.get("name"),
2121
+ description=entity.get("description", ""),
2122
+ status=entity.get("status", 1),
2123
+ )
2124
+ if not response or "Identity" not in response:
2125
+ self.logger.error("Failed to create priority -> '%s'!", entity.get("name"))
2126
+ return False
2127
+ self.logger.info(
2128
+ "Successfully created priority -> '%s' (%s).", entity.get("name"), response["Identity"].get("Id")
2129
+ )
2130
+ self.logger.debug("Response -> %s", str(response))
2131
+ return True
2132
+ case "caseType":
2133
+ case_type = self._otawp.get_case_type_by_name(name=entity.get("name"))
2134
+ if case_type:
2135
+ case_type_id = self._otawp.get_entity_value(entity=case_type, key="id")
2136
+ self.logger.info(
2137
+ "Case type -> '%s' (%s) does already exist. Skipping...", entity.get("name"), str(case_type_id)
2138
+ )
2139
+ return True
2140
+ response = self._otawp.create_case_type(
2141
+ name=entity.get("name"),
2142
+ description=entity.get("description", ""),
2143
+ status=entity.get("status", 1),
2144
+ )
2145
+ if not response or "Identity" not in response:
2146
+ self.logger.error("Failed to case type -> '%s'!", entity.get("name"))
2147
+ return False
2148
+ self.logger.info(
2149
+ "Successfully created case type -> '%s' (%s).", entity.get("name"), response["Identity"].get("Id")
2150
+ )
2151
+ self.logger.debug("Response -> %s", str(response))
2152
+ return True
2153
+ case "customer":
2154
+ customer = self._otawp.get_customer_by_name(name=entity.get("name"))
2155
+ if customer:
2156
+ customer_id = self._otawp.get_entity_value(entity=case_type, key="id")
2157
+ self.logger.info(
2158
+ "Customer -> '%s' (%s) does already exist. Skipping...", entity.get("name"), str(customer_id)
2159
+ )
2160
+ return True
2161
+ response = self._otawp.create_customer(
2162
+ customer_name=entity.get("name"),
2163
+ legal_business_name=entity.get("legal_business_name", ""),
2164
+ trading_name=entity.get("trading_name", ""),
2165
+ )
2166
+ if not response or "Identity" not in response:
2167
+ self.logger.error("Failed to create customer -> '%s'!", entity.get("name"))
2168
+ return False
2169
+ self.logger.info(
2170
+ "Successfully created customer -> '%s' (%s).", entity.get("name"), response["Identity"].get("Id")
2171
+ )
2172
+ self.logger.debug("Response -> %s", str(response))
2173
+ return True
2174
+ case "case":
2175
+ if "subject" not in entity:
2176
+ self.logger.error("Cannot create a case without a subject!")
2177
+ return False
1819
2178
 
1820
- if "projects" in workspace:
1821
- for project in workspace["projects"]:
1822
- if project.get("name") and project.get("documentId"):
1823
- if not self._otawp.publish_project(
1824
- workspace_name=workspace_name,
1825
- project_name=project.get("name"),
1826
- workspace_id=workspace_gui_id,
1827
- project_id=project.get("documentId"),
1828
- ):
1829
- return False
1830
- else:
1831
- self.logger.error(
1832
- "Skipping project -> '%s' due to missing required fields.",
1833
- project.get("name"),
1834
- )
2179
+ category_name = entity.get("category")
2180
+ if category_name:
2181
+ category = self._otawp.get_category_by_name(name=category_name)
2182
+ category_id = self._otawp.get_entity_value(entity=category, key="id")
2183
+ if not category_id:
2184
+ self.logger.error(
2185
+ "Cannot find case category -> '%s' to create case -> '%s'",
2186
+ category_name,
2187
+ entity["subject"],
2188
+ )
2189
+ return False
2190
+ else:
2191
+ self.logger.warning(
2192
+ "Case entity -> '%s' does not have a category specified in its payload!", entity["subject"]
2193
+ )
2194
+ category_id = None
1835
2195
 
1836
- else:
1837
- self.logger.error("Skipping workspace -> '%s' due to missing required fields.", workspace_name)
2196
+ sub_category_name = entity.get("sub_category")
2197
+ if category_id and sub_category_name:
2198
+ sub_category_id = self._otawp.get_sub_category_id(name=sub_category_name, parent_id=category_id)
2199
+ else:
2200
+ sub_category_id = None
1838
2201
 
1839
- self._otawp.create_loanruntime_from_config_file(platform=value)
2202
+ priority_name = entity.get("priority")
2203
+ if priority_name:
2204
+ priority = self._otawp.get_priority_by_name(name=priority_name)
2205
+ priority_id = self._otawp.get_entity_value(entity=priority, key="id")
2206
+ if not priority_id:
2207
+ self.logger.error(
2208
+ "Cannot find case priority -> '%s' to create case -> '%s'",
2209
+ priority_name,
2210
+ entity["subject"],
2211
+ )
2212
+ return False
2213
+ else:
2214
+ self.logger.warning(
2215
+ "Case entity -> '%s' does not have a priority specified in its payload!", entity["subject"]
2216
+ )
2217
+ priority_id = None
1840
2218
 
1841
- return True
2219
+ case_type_name = entity.get("case_type")
2220
+ if case_type_name:
2221
+ case_type = self._otawp.get_case_type_by_name(name=case_type_name)
2222
+ case_type_id = self._otawp.get_entity_value(entity=case_type, key="id")
2223
+ if not case_type_id:
2224
+ self.logger.error(
2225
+ "Cannot find case type -> '%s' to create case -> '%s'!",
2226
+ case_type_name,
2227
+ entity["subject"],
2228
+ )
2229
+ return False
2230
+ else:
2231
+ self.logger.warning(
2232
+ "Case entity -> '%s' does not have a case type specified in its payload!", entity["subject"]
2233
+ )
2234
+ case_type_id = None
2235
+
2236
+ customer_name = entity.get("customer")
2237
+ if customer_name:
2238
+ customer = self._otawp.get_customer_by_name(name=customer_name)
2239
+ customer_id = self._otawp.get_entity_value(entity=customer, key="id")
2240
+ if not customer_id:
2241
+ self.logger.error(
2242
+ "Cannot find customer -> '%s' to create case -> '%s'!",
2243
+ customer_name,
2244
+ entity["subject"],
2245
+ )
2246
+ return False
2247
+ else:
2248
+ self.logger.warning(
2249
+ "Case entity -> '%s' does not have a customer specified in its payload!", entity["subject"]
2250
+ )
2251
+ customer_id = None
2252
+
2253
+ response = self._otawp.create_case(
2254
+ subject=entity.get("subject"),
2255
+ description=entity.get("description", ""),
2256
+ loan_amount=entity.get("loan_amount", 1),
2257
+ loan_duration_in_months=entity.get("loan_duration_in_month", 2),
2258
+ category_id=category_id,
2259
+ sub_category_id=sub_category_id,
2260
+ priority_id=priority_id,
2261
+ case_type_id=case_type_id,
2262
+ customer_id=customer_id,
2263
+ )
2264
+ if not response:
2265
+ self.logger.error("Failed to create case with subject -> '%s'!", entity.get("subject"))
2266
+ return False
2267
+ self.logger.info(
2268
+ "Successfully created case with subject -> '%s'%s.",
2269
+ entity.get("subject"),
2270
+ " for customer with ID -> '{}'".format(customer_id) if customer_id else "",
2271
+ )
2272
+ self.logger.debug("Response -> %s", str(response))
2273
+ return True
2274
+ case _:
2275
+ self.logger.error("Illegal entity type -> '%s'!", entity_type)
2276
+ return False
2277
+
2278
+ return False
1842
2279
 
1843
2280
  # end method definition
1844
2281
 
@@ -2627,6 +3064,9 @@ class Payload:
2627
3064
  )
2628
3065
  workspace_id = self._otcs.get_result_value(response=response, key="id")
2629
3066
  if workspace_id:
3067
+ if not isinstance(workspace_id, int):
3068
+ self.logger.warning("Converting workspace ID -> %s to integer...", str(workspace_id))
3069
+ workspace_id = int(workspace_id)
2630
3070
  # Write nodeID back into the payload
2631
3071
  workspace["nodeId"] = workspace_id
2632
3072
  return workspace_id
@@ -2769,19 +3209,24 @@ class Payload:
2769
3209
  continue
2770
3210
 
2771
3211
  match payload_section["name"]:
2772
- case "feme":
3212
+ case "embeddings" | "feme":
2773
3213
  self._log_header_callback(
2774
- text="Process Feme for Content Aviator Metadata embedding",
3214
+ text="Process additional Embeddings for Content Aviator",
2775
3215
  )
2776
- self.process_feme()
3216
+ self.process_embeddings()
2777
3217
  case "avtsRepositories":
2778
3218
  self._log_header_callback(
2779
3219
  text="Process Aviator Search repositories",
2780
3220
  )
2781
3221
  self.process_avts_repositories()
2782
- case "platformCustomConfig":
2783
- self._log_header_callback(text="Process Create AppWorks workspaces")
2784
- self.ot_awp_create_project()
3222
+ case "avtsQuestions":
3223
+ self._log_header_callback(
3224
+ text="Process Aviator Search Sample Questions",
3225
+ )
3226
+ self.process_avts_questions()
3227
+ case "appworks":
3228
+ self._log_header_callback(text="Process AppWorks Configurations")
3229
+ self.process_appworks_configurations()
2785
3230
  case "webHooks":
2786
3231
  self._log_header_callback(text="Process Web Hooks")
2787
3232
  self.process_web_hooks(webhooks=self._webhooks)
@@ -2797,11 +3242,9 @@ class Payload:
2797
3242
  case "partitions":
2798
3243
  self._log_header_callback(text="Process OTDS Partitions")
2799
3244
  self.process_partitions()
2800
- self._log_header_callback(
2801
- text="Assign OTCS Licenses to Partitions",
2802
- char="-",
2803
- )
2804
- self.process_partition_licenses()
3245
+ case "licenses":
3246
+ self._log_header_callback(text="Process OTDS Licenses")
3247
+ self.process_licenses()
2805
3248
  case "synchronizedPartitions":
2806
3249
  self._log_header_callback(
2807
3250
  text="Process OTDS Synchronized Partitions",
@@ -2835,12 +3278,12 @@ class Payload:
2835
3278
  # if the customizer pod is restarted / run multiple times:
2836
3279
  self.process_group_placeholders()
2837
3280
  if self._core_share and isinstance(self._core_share, CoreShare):
2838
- self._log_header_callback(text="Process Core Share Groups")
3281
+ self._log_header_callback(text="Process Core Share Groups", char="-")
2839
3282
  self.process_groups_core_share()
2840
3283
  if self._m365 and isinstance(self._m365, M365):
2841
- self._log_header_callback(text="Cleanup existing M365 Teams")
3284
+ self._log_header_callback(text="Cleanup existing M365 Teams", char="-")
2842
3285
  self.cleanup_all_teams_m365()
2843
- self._log_header_callback(text="Process M365 Groups")
3286
+ self._log_header_callback(text="Process M365 Groups", char="-")
2844
3287
  self.process_groups_m365()
2845
3288
  case "users":
2846
3289
  self._log_header_callback(text="Process Users")
@@ -2860,46 +3303,25 @@ class Payload:
2860
3303
  self.process_user_licenses(
2861
3304
  resource_name=self._otcs.config()["resource"],
2862
3305
  license_feature=self._otcs.config()["license"],
2863
- license_name="EXTENDED_ECM",
2864
3306
  user_specific_payload_field="licenses",
2865
3307
  )
2866
- self._log_header_callback(
2867
- text="Assign OTIV licenses to users",
2868
- char="-",
2869
- )
2870
-
2871
- if (
2872
- isinstance(self._otiv, OTIV) # can be None in 24.1 or newer
2873
- and self._otiv.config()
2874
- and self._otiv.config()["resource"]
2875
- and self._otiv.config()["license"]
2876
- ):
2877
- self.process_user_licenses(
2878
- resource_name=self._otiv.config()["resource"],
2879
- license_feature=self._otiv.config()["license"],
2880
- license_name="INTELLIGENT_VIEWING",
2881
- user_specific_payload_field="",
2882
- section_name="userLicensesViewing", # we need a specific name here for OTIV
2883
- )
2884
- else:
2885
- self.logger.info("Processing of OTIV licenses is disabled.")
2886
3308
  self._log_header_callback(
2887
3309
  text="Process OTDS user settings",
2888
3310
  char="-",
2889
3311
  )
2890
3312
  self.process_user_settings()
2891
3313
  if self._core_share and isinstance(self._core_share, CoreShare):
2892
- self._log_header_callback(text="Process Core Share users")
3314
+ self._log_header_callback(text="Process Core Share users", char="-")
2893
3315
  self.process_users_core_share()
2894
3316
  if self._m365 and isinstance(self._m365, M365):
2895
- self._log_header_callback(text="Process M365 users")
3317
+ self._log_header_callback(text="Process M365 users", char="-")
2896
3318
  self.process_users_m365()
2897
3319
  # We need to do the MS Teams creation after the creation of
2898
3320
  # the M365 users as we require Group Owners to create teams.
2899
3321
  # Note: this is just for the teams of the top-level OTCS groups
2900
3322
  # (departments), not the MS Teams for the Workspaces. These
2901
3323
  # are created via the scheduled bots!
2902
- self._log_header_callback(text="Process M365 Teams for departmental groups")
3324
+ self._log_header_callback(text="Process M365 Teams for departmental groups", char="-")
2903
3325
  self.process_teams_m365()
2904
3326
  case "adminSettings":
2905
3327
  self._log_header_callback(
@@ -2937,9 +3359,15 @@ class Payload:
2937
3359
  case "execPodCommands":
2938
3360
  self._log_header_callback(text="Process Pod Commands")
2939
3361
  self.process_exec_pod_commands()
3362
+ case "kubernetes":
3363
+ self._log_header_callback(text="Process Kubernetes Commands")
3364
+ self.process_kubernetes()
2940
3365
  case "execCommands":
2941
- self._log_header_callback(text="Process execCommands")
3366
+ self._log_header_callback(text="Process Commands in Customizer Pod")
2942
3367
  self.process_exec_commands()
3368
+ case "execDatabaseCommands":
3369
+ self._log_header_callback(text="Process Database Commands")
3370
+ self.process_exec_database_commands()
2943
3371
  case "csApplications":
2944
3372
  self._log_header_callback(text="Process CS Apps (backend)")
2945
3373
  self.process_cs_applications(
@@ -3209,6 +3637,11 @@ class Payload:
3209
3637
  browser_automations=self._browser_automations_post,
3210
3638
  section_name="browserAutomationsPost",
3211
3639
  )
3640
+ case "testAutomations":
3641
+ self._log_header_callback(text="Process Test Automations")
3642
+ self.process_browser_automations(
3643
+ browser_automations=self._test_automations, section_name="testAutomations"
3644
+ )
3212
3645
  case "workspaceTypes":
3213
3646
  if not self._workspace_types:
3214
3647
  self._log_header_callback(text="Process Workspace Types")
@@ -3250,7 +3683,7 @@ class Payload:
3250
3683
  if self._salesforce and isinstance(self._salesforce, Salesforce):
3251
3684
  self._log_header_callback("Process Salesforce User Profile Photos")
3252
3685
  self.process_user_photos_salesforce()
3253
- if self._salesforce and isinstance(self._core_share, CoreShare):
3686
+ if self._core_share and isinstance(self._core_share, CoreShare):
3254
3687
  self._log_header_callback("Process Core Share User Profile Photos")
3255
3688
  self.process_user_photos_core_share()
3256
3689
  self._log_header_callback("Process User Favorites and Profiles")
@@ -3526,34 +3959,33 @@ class Payload:
3526
3959
  response = self._otds.get_partition(name=partition_name, show_error=False)
3527
3960
  if response:
3528
3961
  self.logger.info(
3529
- "Synchronized partition -> '%s' does already exist. Skipping...",
3962
+ "Synchronized partition -> '%s' does already exist.",
3530
3963
  partition_name,
3531
3964
  )
3532
- continue
3533
-
3534
- # Only continue if synchronized Partition does not exist already
3535
- self.logger.info(
3536
- "Synchronized partition -> '%s' does not yet exist. Creating...",
3537
- partition_name,
3538
- )
3539
-
3540
- response = self._otds.add_synchronized_partition(
3541
- name=partition_name,
3542
- description=partition_description,
3543
- data=partition["spec"],
3544
- )
3545
- if response:
3965
+ else:
3966
+ # Only continue if synchronized Partition does not exist already
3546
3967
  self.logger.info(
3547
- "Added synchronized partition -> '%s' to OTDS.",
3968
+ "Synchronized partition -> '%s' does not yet exist. Creating...",
3548
3969
  partition_name,
3549
3970
  )
3550
- else:
3551
- self.logger.error(
3552
- "Failed to add synchronized partition -> '%s' to OTDS!",
3553
- partition_name,
3971
+
3972
+ response = self._otds.add_synchronized_partition(
3973
+ name=partition_name,
3974
+ description=partition_description,
3975
+ data=partition["spec"],
3554
3976
  )
3555
- success = False
3556
- continue
3977
+ if response:
3978
+ self.logger.info(
3979
+ "Added synchronized partition -> '%s' to OTDS.",
3980
+ partition_name,
3981
+ )
3982
+ else:
3983
+ self.logger.error(
3984
+ "Failed to add synchronized partition -> '%s' to OTDS!",
3985
+ partition_name,
3986
+ )
3987
+ success = False
3988
+ continue
3557
3989
 
3558
3990
  result = self._otds.import_synchronized_partition_members(
3559
3991
  name=partition_name,
@@ -3569,7 +4001,6 @@ class Payload:
3569
4001
  partition_name,
3570
4002
  )
3571
4003
  success = False
3572
- continue
3573
4004
 
3574
4005
  access_role = partition.get("access_role")
3575
4006
  if access_role:
@@ -3590,7 +4021,6 @@ class Payload:
3590
4021
  access_role,
3591
4022
  )
3592
4023
  success = False
3593
- continue
3594
4024
  # end if access_role
3595
4025
 
3596
4026
  # Partions may have an optional list of licenses in
@@ -3608,11 +4038,54 @@ class Payload:
3608
4038
  success = False
3609
4039
  continue
3610
4040
  otcs_resource_id = otcs_resource["resourceID"]
3611
- license_name = "EXTENDED_ECM"
3612
- for license_feature in partition_specific_licenses:
4041
+ otcs_license_name = "EXTENDED_ECM"
4042
+ for license_item in partition_specific_licenses:
4043
+ if isinstance(license_item, dict):
4044
+ license_feature = license_item.get("feature")
4045
+ license_name = license_item.get("name", "EXTENDED_ECM")
4046
+ if "enabled" in license_item and not license_item["enabled"]:
4047
+ self.logger.info(
4048
+ "Payload for License '%s' -> '%s' is disabled. Skipping...",
4049
+ license_name,
4050
+ license_feature,
4051
+ )
4052
+ continue
4053
+ if "resource" in license_item:
4054
+ try:
4055
+ resource_id = self._otds.get_resource(name=license_item["resource"])["resourceID"]
4056
+ except Exception:
4057
+ self.logger.error(
4058
+ "Error getting resourceID from resource -> %s", license_item["resource"]
4059
+ )
4060
+ else:
4061
+ resource_id = otcs_resource_id
4062
+
4063
+ elif isinstance(license_item, str):
4064
+ license_feature = license_item
4065
+ resource_id = otcs_resource_id
4066
+ license_name = otcs_license_name
4067
+ else:
4068
+ self.logger.error("Invalid License feature specified -> %s", license_item)
4069
+ success = False
4070
+ continue
4071
+
4072
+ if self._otds.is_partition_licensed(
4073
+ partition_name=partition_name,
4074
+ resource_id=resource_id,
4075
+ license_feature=license_feature,
4076
+ license_name=license_name,
4077
+ ):
4078
+ self.logger.info(
4079
+ "Partition -> '%s' is already licensed for -> '%s' (%s)",
4080
+ partition_name,
4081
+ license_name,
4082
+ license_feature,
4083
+ )
4084
+ continue
4085
+
3613
4086
  assigned_license = self._otds.assign_partition_to_license(
3614
4087
  partition_name=partition_name,
3615
- resource_id=otcs_resource_id,
4088
+ resource_id=resource_id,
3616
4089
  license_feature=license_feature,
3617
4090
  license_name=license_name,
3618
4091
  )
@@ -3701,34 +4174,33 @@ class Payload:
3701
4174
  response = self._otds.get_partition(name=partition_name, show_error=False)
3702
4175
  if response:
3703
4176
  self.logger.info(
3704
- "Partition -> '%s' does already exist. Skipping...",
4177
+ "Partition -> '%s' does already exist.",
3705
4178
  partition_name,
3706
4179
  )
3707
- continue
3708
-
3709
- # Only continue if Partition does not exist already
3710
- self.logger.info(
3711
- "Partition -> '%s' does not yet exist. Creating...",
3712
- partition_name,
3713
- )
3714
-
3715
- response = self._otds.add_partition(
3716
- name=partition_name,
3717
- description=partition_description,
3718
- )
3719
- if response:
4180
+ else:
4181
+ # Only continue if Partition does not exist already
3720
4182
  self.logger.info(
3721
- "Added OTDS partition -> '%s'%s.",
4183
+ "Partition -> '%s' does not yet exist. Creating...",
3722
4184
  partition_name,
3723
- " ({})".format(partition_description) if partition_description else "",
3724
4185
  )
3725
- else:
3726
- self.logger.error(
3727
- "Failed to add OTDS partition -> '%s'!",
3728
- partition_name,
4186
+
4187
+ response = self._otds.add_partition(
4188
+ name=partition_name,
4189
+ description=partition_description,
3729
4190
  )
3730
- success = False
3731
- continue
4191
+ if response:
4192
+ self.logger.info(
4193
+ "Added OTDS partition -> '%s'%s.",
4194
+ partition_name,
4195
+ " ({})".format(partition_description) if partition_description else "",
4196
+ )
4197
+ else:
4198
+ self.logger.error(
4199
+ "Failed to add OTDS partition -> '%s'!",
4200
+ partition_name,
4201
+ )
4202
+ success = False
4203
+ continue
3732
4204
 
3733
4205
  access_role = partition.get("access_role")
3734
4206
  if access_role:
@@ -3749,7 +4221,6 @@ class Payload:
3749
4221
  access_role,
3750
4222
  )
3751
4223
  success = False
3752
- continue
3753
4224
  # end if access_role
3754
4225
 
3755
4226
  # Partions may have an optional list of licenses in
@@ -3767,11 +4238,55 @@ class Payload:
3767
4238
  success = False
3768
4239
  continue
3769
4240
  otcs_resource_id = otcs_resource["resourceID"]
3770
- license_name = "EXTENDED_ECM"
3771
- for license_feature in partition_specific_licenses:
4241
+ otcs_license_name = "EXTENDED_ECM"
4242
+ for license_item in partition_specific_licenses:
4243
+ if isinstance(license_item, dict):
4244
+ license_feature = license_item.get("feature")
4245
+ license_name = license_item.get("name", "EXTENDED_ECM")
4246
+ if "enabled" in license_item and not license_item["enabled"]:
4247
+ self.logger.info(
4248
+ "Payload for License '%s' -> '%s' is disabled. Skipping...",
4249
+ license_name,
4250
+ license_feature,
4251
+ )
4252
+ continue
4253
+
4254
+ if "resource" in license_item:
4255
+ try:
4256
+ resource_id = self._otds.get_resource(name=license_item["resource"])["resourceID"]
4257
+ except Exception:
4258
+ self.logger.error(
4259
+ "Error getting resourceID from resource -> %s", license_item["resource"]
4260
+ )
4261
+ else:
4262
+ resource_id = otcs_resource_id
4263
+
4264
+ elif isinstance(license_item, str):
4265
+ license_feature = license_item
4266
+ resource_id = otcs_resource_id
4267
+ license_name = otcs_license_name
4268
+ else:
4269
+ self.logger.error("Invalid License feature specified -> %s", license_item)
4270
+ success = False
4271
+ continue
4272
+
4273
+ if self._otds.is_partition_licensed(
4274
+ partition_name=partition_name,
4275
+ resource_id=resource_id,
4276
+ license_feature=license_feature,
4277
+ license_name=license_name,
4278
+ ):
4279
+ self.logger.info(
4280
+ "Partition -> '%s' is already licensed for -> '%s' (%s)",
4281
+ partition_name,
4282
+ license_name,
4283
+ license_feature,
4284
+ )
4285
+ continue
4286
+
3772
4287
  assigned_license = self._otds.assign_partition_to_license(
3773
4288
  partition_name=partition_name,
3774
- resource_id=otcs_resource_id,
4289
+ resource_id=resource_id,
3775
4290
  license_feature=license_feature,
3776
4291
  license_name=license_name,
3777
4292
  )
@@ -3804,13 +4319,8 @@ class Payload:
3804
4319
 
3805
4320
  # end method definition
3806
4321
 
3807
- def process_partition_licenses(
3808
- self,
3809
- section_name: str = "partitionLicenses",
3810
- ) -> bool:
3811
- """Process the licenses that should be assigned to OTDS partitions.
3812
-
3813
- (this includes existing partitions)
4322
+ def process_licenses(self, section_name: str = "licenses") -> bool:
4323
+ """Process OTDS licenses in payload and create them in OTDS.
3814
4324
 
3815
4325
  Args:
3816
4326
  section_name (str, optional):
@@ -3822,11 +4332,11 @@ class Payload:
3822
4332
 
3823
4333
  Returns:
3824
4334
  bool:
3825
- True, if payload has been processed without errors, False otherwise.
4335
+ True if payload has been processed without errors, False otherwise
3826
4336
 
3827
4337
  """
3828
4338
 
3829
- if not self._partitions:
4339
+ if not self._licenses:
3830
4340
  self.logger.info(
3831
4341
  "Payload section -> '%s' is empty. Skipping...",
3832
4342
  section_name,
@@ -3840,83 +4350,52 @@ class Payload:
3840
4350
 
3841
4351
  success: bool = True
3842
4352
 
3843
- for partition in self._partitions:
3844
- partition_name = partition.get("name")
3845
- if not partition_name:
3846
- self.logger.error("Partition does not have a name. Skipping...")
3847
- success = False
3848
- continue
3849
-
3850
- # Check if partition has been explicitly disabled in payload
3851
- # (enabled = false). In this case we skip the element:
3852
- if not partition.get("enabled", True):
4353
+ for lic in self._licenses:
4354
+ self.logger.debug("Start processing License -> '%s'", lic)
4355
+ if not lic.get("enabled", True):
3853
4356
  self.logger.info(
3854
- "Payload for Partition -> '%s' is disabled. Skipping...",
3855
- partition_name,
4357
+ "Payload for License -> '%s' is disabled. Skipping...",
4358
+ lic,
3856
4359
  )
3857
4360
  continue
3858
4361
 
3859
- response = self._otds.get_partition(name=partition_name, show_error=True)
3860
- if not response:
3861
- self.logger.error(
3862
- "Partition -> '%s' does not exist. Skipping...",
3863
- partition_name,
3864
- )
4362
+ path = lic.get("path")
4363
+ if not path:
4364
+ self.logger.error("Required attribute path not specified (%s). Skipping ...", lic)
3865
4365
  success = False
3866
4366
  continue
3867
4367
 
3868
- # Partions may have an optional list of licenses in
3869
- # the payload. Assign the partition to all these licenses:
3870
- partition_specific_licenses = partition.get("licenses")
3871
- if partition_specific_licenses:
3872
- # We assume these licenses are Extended ECM licenses!
3873
- otcs_resource_name = self._otcs.config()["resource"]
3874
- otcs_resource = self._otds.get_resource(name=otcs_resource_name)
3875
- if not otcs_resource:
3876
- self.logger.error(
3877
- "Cannot find OTCS resource -> '%s'",
3878
- otcs_resource_name,
3879
- )
4368
+ product_name = lic.get("product_name")
4369
+ if not product_name:
4370
+ self.logger.error("Required attribute product_name not specified (%s). Skipping ...", lic)
4371
+ success = False
4372
+ continue
4373
+
4374
+ if "resource" in lic:
4375
+ try:
4376
+ resource_id = self._otds.get_resource(name=lic["resource"])["resourceID"]
4377
+ except Exception:
4378
+ self.logger.error("Error getting resourceID from resource -> %s", lic["resource"])
3880
4379
  success = False
3881
4380
  continue
3882
- otcs_resource_id = otcs_resource["resourceID"]
3883
- license_name = "EXTENDED_ECM"
3884
- for license_feature in partition_specific_licenses:
3885
- if self._otds.is_partition_licensed(
3886
- partition_name=partition_name,
3887
- resource_id=otcs_resource_id,
3888
- license_feature=license_feature,
3889
- license_name=license_name,
3890
- ):
3891
- self.logger.info(
3892
- "Partition -> '%s' is already licensed for -> '%s' ('%s')",
3893
- partition_name,
3894
- license_name,
3895
- license_feature,
3896
- )
3897
- continue
3898
- assigned_license = self._otds.assign_partition_to_license(
3899
- partition_name=partition_name,
3900
- resource_id=otcs_resource_id,
3901
- license_feature=license_feature,
3902
- license_name=license_name,
3903
- )
4381
+ else:
4382
+ self.logger.error("Required attribute resource not specified (%s). Skipping ...", lic)
4383
+ success = False
4384
+ continue
3904
4385
 
3905
- if not assigned_license:
3906
- self.logger.error(
3907
- "Failed to assign partition -> '%s' to license feature -> '%s' of license -> '%s'!",
3908
- partition_name,
3909
- license_feature,
3910
- license_name,
3911
- )
3912
- success = False
3913
- else:
3914
- self.logger.info(
3915
- "Successfully assigned partition -> '%s' to license feature -> '%s' of license -> '%s'",
3916
- partition_name,
3917
- license_feature,
3918
- license_name,
3919
- )
4386
+ self.logger.info("Adding License %s for %s to resource '%s'", path, product_name, resource_id)
4387
+
4388
+ add_license = self._otds.add_license_to_resource(
4389
+ path_to_license_file=path,
4390
+ product_name=product_name,
4391
+ product_description=lic.get("description", ""),
4392
+ resource_id=resource_id,
4393
+ update=lic.get("update", True),
4394
+ )
4395
+
4396
+ if not add_license:
4397
+ success = False
4398
+ continue
3920
4399
 
3921
4400
  self.write_status_file(
3922
4401
  success=success,
@@ -6477,7 +6956,8 @@ class Payload:
6477
6956
  user_last_name = user.get("lastname", "") # Default is important here
6478
6957
  user_first_name = user.get("firstname", "")
6479
6958
  user_name = " ".join(filter(None, [user_first_name, user_last_name]))
6480
- if not user_name:
6959
+ user_enabled = user.get("enabled", True) and user.get("enable_core_share", False)
6960
+ if not user_name and user_enabled: # Avoid a warning if not enbaled
6481
6961
  self.logger.error(
6482
6962
  "User is missing last name and first name. Skipping to next user...",
6483
6963
  )
@@ -7318,23 +7798,23 @@ class Payload:
7318
7798
  parent_group_name,
7319
7799
  parent_group_id,
7320
7800
  )
7321
- continue
7322
-
7323
- self.logger.info(
7324
- "Add Microsoft 365 user -> '%s' (%s) to Microsoft 365 group -> '%s' (%s)",
7325
- user["name"],
7326
- m365_user_id,
7327
- parent_group_name,
7328
- parent_group_id,
7329
- )
7330
- self._m365.add_group_member(
7331
- group_id=parent_group_id,
7332
- member_id=m365_user_id,
7333
- )
7801
+ else:
7802
+ self.logger.info(
7803
+ "Add Microsoft 365 user -> '%s' (%s) to Microsoft 365 group -> '%s' (%s)...",
7804
+ user["name"],
7805
+ m365_user_id,
7806
+ parent_group_name,
7807
+ parent_group_id,
7808
+ )
7809
+ self._m365.add_group_member(
7810
+ group_id=parent_group_id,
7811
+ member_id=m365_user_id,
7812
+ )
7334
7813
  # end for parent_group_name
7335
7814
 
7336
- # Make this user follow the SharePoint site of his/her department:
7337
- if group_name == user_department:
7815
+ # Make this user follow the SharePoint site of his/her department.
7816
+ # We only do this for users that have a valid M365 license (SKU):
7817
+ if group_name == user_department and user["m365_skus"]:
7338
7818
  group = self._m365.get_group(group_name=group_name)
7339
7819
  group_id = self._m365.get_result_value(response=group, key="id")
7340
7820
  if group_id:
@@ -7342,19 +7822,48 @@ class Payload:
7342
7822
  group_site_id = self._m365.get_result_value(response=group_site, key="id")
7343
7823
  if group_site_id:
7344
7824
  group_site_name = self._m365.get_result_value(response=group_site, key="name")
7345
- response = self._m365.follow_sharepoint_site(site_id=group_site_id, user_id=m365_user_id)
7346
- if response:
7347
- self.logger.info(
7348
- "Make user -> '%s' follow SharePoint site -> '%s'",
7349
- user["name"],
7350
- group_site_name,
7825
+ # Make sure the user's mysite (drive) has been provisioned.
7826
+ # This is a prerequisite for a user being able to follow a
7827
+ # SharePoint site. For this to work we need "Files.ReadWrite"
7828
+ # as a delegated permission in the Azure AppRegistration!
7829
+ self.logger.info(
7830
+ "Make sure user -> '%s' has a drive (mySite) provisioned...",
7831
+ user["email"],
7832
+ )
7833
+ # We need to have 'delegated' permissions for this so we authenticate
7834
+ # as the user... The scope is important here - the user's drive can only
7835
+ # be provisioned if "Files.ReadWrite" scope is provided:
7836
+ response = self._m365.authenticate_user(
7837
+ username=user["email"], password=user["password"], scope="Files.ReadWrite"
7838
+ )
7839
+ if not response:
7840
+ self.logger.error(
7841
+ "Couldn't authenticate as M365 user -> '%s' to provision user's drive!",
7842
+ username=user["email"],
7351
7843
  )
7352
- else:
7844
+ success = False
7845
+ continue
7846
+ # Retrieve the drive endpoint to trigger the drive provisioning. It is important
7847
+ # to use the 'me=True' to make sure the request is done with the user credentials,
7848
+ # not the app credentials (using purely client_id / client_secret):
7849
+ response = self._m365.get_user_drive(user_id=user["email"], me=True)
7850
+ if not response:
7851
+ self.logger.error("Couldn't get M365 drive of user -> '%s'!", user["email"])
7852
+ success = False
7853
+ continue
7854
+ self.logger.info(
7855
+ "Make user -> '%s' follow departmental SharePoint site -> '%s'...",
7856
+ user["email"],
7857
+ group_site_name,
7858
+ )
7859
+ response = self._m365.follow_sharepoint_site(site_id=group_site_id, user_id=m365_user_id)
7860
+ if not response:
7353
7861
  self.logger.warning(
7354
7862
  "User -> '%s' cannot follow SharePoint site -> '%s'. ",
7355
- user["name"],
7863
+ user["email"],
7356
7864
  group_site_name,
7357
7865
  )
7866
+ success = False
7358
7867
  # end for group name
7359
7868
  # end for user
7360
7869
  self.write_status_file(
@@ -10082,12 +10591,18 @@ class Payload:
10082
10591
  self._workspace_types = self.get_status_file(
10083
10592
  payload_section_name=section_name,
10084
10593
  )
10085
- self.logger.info(
10086
- "Found -> %s workspace types.",
10087
- str(len(self._workspace_types)),
10088
- )
10089
- self.logger.debug("Workspace types -> %s", str(self._workspace_types))
10090
- return self._workspace_types
10594
+ if self._workspace_types:
10595
+ self.logger.info(
10596
+ "Found -> %s workspace types.",
10597
+ str(len(self._workspace_types)),
10598
+ )
10599
+ self.logger.debug("Workspace types -> %s", str(self._workspace_types))
10600
+ return self._workspace_types
10601
+ else:
10602
+ self.logger.error(
10603
+ "Couldn't read workspace types from status file -> '%s'. Regenerate list...",
10604
+ self.get_status_file_name(payload_section_name=section_name),
10605
+ )
10091
10606
 
10092
10607
  # Read payload_section "workspaceTypes" if available
10093
10608
  payload_section = {}
@@ -11087,17 +11602,18 @@ class Payload:
11087
11602
  search_value=search_value,
11088
11603
  result_fields=["Id"],
11089
11604
  )
11605
+ bo_id = self._salesforce.get_result_value(response=response, key="Id")
11090
11606
  num_of_bos = int(response.get("totalSize", 0)) if (response is not None and "totalSize" in response) else 0
11091
11607
  if num_of_bos > 1:
11092
11608
  self.logger.warning(
11093
- "Salesforce lookup delivered %s business objects for business object type -> '%s'! We will pick the first one.",
11609
+ "Salesforce lookup delivered %s business objects for business object type -> '%s'! We will pick the first one -> %s.",
11094
11610
  str(num_of_bos),
11095
11611
  str(object_type),
11612
+ bo_id,
11096
11613
  )
11097
- bo_id = self._salesforce.get_result_value(response=response, key="Id")
11098
11614
  if not bo_id:
11099
11615
  self.logger.warning(
11100
- "Business object of type -> '%s' and %s = %s does not exist in Salesforce!",
11616
+ "Business object of type -> '%s' with '%s' = '%s' does not exist in Salesforce!",
11101
11617
  object_type,
11102
11618
  search_field,
11103
11619
  search_value,
@@ -12483,7 +12999,7 @@ class Payload:
12483
12999
  )
12484
13000
 
12485
13001
  # now determine the actual node IDs of the workspaces (has been created before):
12486
- workspace_node_id: int = self.determine_workspace_id(workspace=workspace)
13002
+ workspace_node_id = int(self.determine_workspace_id(workspace=workspace))
12487
13003
  if not workspace_node_id:
12488
13004
  self.logger.warning(
12489
13005
  "Workspace -> '%s' (type -> '%s') has no node ID and cannot have a relationship (workspace creation may have failed or final name is different from payload). Skipping to next workspace...",
@@ -12501,39 +13017,62 @@ class Payload:
12501
13017
  success: bool = True
12502
13018
 
12503
13019
  for related_workspace_id in workspace["relationships"]:
12504
- # Find the workspace type with the name given in the payload:
13020
+ # Initialize variable to determine if we found a related workspace:
13021
+ related_workspace_node_id = None
13022
+
13023
+ #
13024
+ # 1. Option: Find the related workspace with the logical ID given in the payload:
13025
+ #
12505
13026
  related_workspace = next(
12506
13027
  (item for item in self._workspaces if item["id"] == related_workspace_id),
12507
13028
  None,
12508
13029
  )
12509
- if related_workspace is None:
12510
- self.logger.error(
12511
- "Related Workspace with logical ID -> %s not found.",
13030
+ if related_workspace:
13031
+ if not related_workspace.get("enabled", True):
13032
+ self.logger.info(
13033
+ "Payload for Related Workspace -> '%s' is disabled. Skipping...",
13034
+ related_workspace["name"],
13035
+ )
13036
+ continue
13037
+
13038
+ related_workspace_node_id = self.determine_workspace_id(
13039
+ workspace=related_workspace,
13040
+ )
13041
+ if not related_workspace_node_id:
13042
+ self.logger.warning(
13043
+ "Related Workspace -> '%s' (type -> '%s') has no node ID (workspaces creation may have failed or name is different from payload). Skipping to next workspace...",
13044
+ related_workspace["name"],
13045
+ related_workspace["type_name"],
13046
+ )
13047
+ continue
13048
+ self.logger.debug(
13049
+ "Related Workspace with logical ID -> %s has node ID -> %s",
12512
13050
  related_workspace_id,
13051
+ related_workspace_node_id,
12513
13052
  )
12514
- success = False
12515
- continue
13053
+ # end if related_workspace is not None
12516
13054
 
12517
- if not related_workspace.get("enabled", True):
12518
- self.logger.info(
12519
- "Payload for Related Workspace -> '%s' is disabled. Skipping...",
12520
- related_workspace["name"],
13055
+ #
13056
+ # 2. Option: Find the related workspace with nickname:
13057
+ #
13058
+ else:
13059
+ # See if a nickname exists the the provided related_workspace_id:
13060
+ response = self._otcs.get_node_from_nickname(nickname=related_workspace_id)
13061
+ related_workspace_node_id = self._otcs.get_result_value(
13062
+ response=response,
13063
+ key="id",
12521
13064
  )
12522
- continue
12523
13065
 
12524
- related_workspace_node_id = self.determine_workspace_id(
12525
- workspace=related_workspace,
12526
- )
12527
- if not related_workspace_node_id:
12528
- self.logger.warning(
12529
- "Related Workspace -> '%s' (type -> '%s') has no node ID (workspaces creation may have failed or name is different from payload). Skipping to next workspace...",
12530
- related_workspace["name"],
12531
- related_workspace["type_name"],
13066
+ if related_workspace_node_id is None:
13067
+ self.logger.error(
13068
+ "Related Workspace with logical ID or nickname -> %s not found.",
13069
+ related_workspace_id,
12532
13070
  )
13071
+ success = False
12533
13072
  continue
12534
13073
 
12535
13074
  self.logger.debug(
12536
- "Related Workspace with logical ID -> %s has node ID -> %s",
13075
+ "Related Workspace with logical ID or nickname -> %s has node ID -> %s",
12537
13076
  related_workspace_id,
12538
13077
  related_workspace_node_id,
12539
13078
  )
@@ -13327,6 +13866,15 @@ class Payload:
13327
13866
  )
13328
13867
  success = False
13329
13868
  continue
13869
+ # Also embed the workspace metadata:
13870
+ if not self._otcs.aviator_embed_metadata(
13871
+ node_id=workspace_id,
13872
+ workspace_metadata=True,
13873
+ wait_for_completion=False,
13874
+ ):
13875
+ success = False
13876
+ continue
13877
+ # end for workspace in self._workspaces
13330
13878
 
13331
13879
  self.write_status_file(
13332
13880
  success=success,
@@ -13867,6 +14415,7 @@ class Payload:
13867
14415
  )
13868
14416
  if not favorite_item:
13869
14417
  self.logger.error("Cannot find path -> %s for favorite item!", str(favorite))
14418
+ success = False
13870
14419
  continue
13871
14420
  favorite_id = self._otcs.get_result_value(
13872
14421
  response=favorite_item,
@@ -14998,6 +15547,23 @@ class Payload:
14998
15547
  items (list):
14999
15548
  List of items to create (need this as parameter as we
15000
15549
  have multiple lists).
15550
+ Each list item in the payload is a dict with this structure:
15551
+ {
15552
+ enabled = "..."
15553
+ name = "..."
15554
+ description = "..."
15555
+ nickname = "..."
15556
+ parent_nickname = "..."
15557
+ parent_path = [...]
15558
+ parent_volume = ...
15559
+ original_nickname = "..."
15560
+ original_path = []
15561
+ type = ...
15562
+ url = "..."
15563
+ details = {
15564
+ "scheduledbotdetails" : ...
15565
+ } # additional parameters
15566
+ }
15001
15567
  section_name (str, optional):
15002
15568
  The name of the payload section. It can be overridden
15003
15569
  for cases where multiple sections of same type
@@ -15042,6 +15608,7 @@ class Payload:
15042
15608
  continue
15043
15609
 
15044
15610
  item_description = item.get("description", "")
15611
+ item_nickname = item.get("nickname", None)
15045
15612
  parent_nickname = item.get("parent_nickname", None)
15046
15613
  parent_path = item.get("parent_path", None)
15047
15614
 
@@ -15124,7 +15691,7 @@ class Payload:
15124
15691
  item_type = int(item.get("type", self._otcs.ITEM_TYPE_FOLDER))
15125
15692
  item_url = item.get("url", "")
15126
15693
  item_details = item.get("details", {})
15127
-
15694
+ create_item_details = item.get("create_details", {})
15128
15695
  # check that we have the required information
15129
15696
  # for the given item type:
15130
15697
  match item_type:
@@ -15164,6 +15731,8 @@ class Payload:
15164
15731
  success = False
15165
15732
  continue
15166
15733
 
15734
+ create_item_details["xecmpfJobType"] = item_details["xecmpfJobType"]
15735
+
15167
15736
  # Check if an item with the same name does already exist.
15168
15737
  # This can also be the case if the python container runs a 2nd time.
15169
15738
  # For this reason we are also not issuing an error but just an info (False):
@@ -15186,7 +15755,7 @@ class Payload:
15186
15755
  item_description=item_description,
15187
15756
  url=item_url,
15188
15757
  original_id=int(original_id),
15189
- **item_details,
15758
+ **create_item_details,
15190
15759
  )
15191
15760
  node_id = self._otcs.get_result_value(response=response, key="id")
15192
15761
  if not node_id:
@@ -15195,26 +15764,80 @@ class Payload:
15195
15764
  continue
15196
15765
 
15197
15766
  self.logger.info(
15198
- "Item -> '%s' with ID -> %s has been created successfully.",
15767
+ "Successfully created item -> '%s' with ID -> %s.",
15199
15768
  item_name,
15200
15769
  node_id,
15201
15770
  )
15202
15771
 
15203
15772
  # Special handling for scheduled bot items:
15204
15773
  if item_type == self._otcs.ITEM_TYPE_SCHEDULED_BOT:
15774
+ scheduled_bot_details = item_details.get("scheduledbotdetails", {})
15775
+ if not scheduled_bot_details:
15776
+ self.logger.error("Failed to get details of scheduled bot item -> '%s'.", item_name)
15777
+ success = False
15778
+ continue
15779
+ start_mode = scheduled_bot_details.get("startmodus")
15780
+ if not start_mode:
15781
+ self.logger.error("Failed to get start mode of scheduled bot item -> '%s'.", item_name)
15782
+ success = False
15783
+ continue
15784
+ start_mode = start_mode.get("startMode")
15785
+ if not start_mode:
15786
+ self.logger.error("Failed to get start mode of scheduled bot item -> '%s'.", item_name)
15787
+ success = False
15788
+ continue
15789
+ old_schedule_data = scheduled_bot_details.get("oldscheduleData")
15790
+ # Check if this bot should start after another bot:
15791
+ if start_mode == "AfterJob":
15792
+ after_job_nickname = scheduled_bot_details["startmodus"].get("afterJob")
15793
+ if not after_job_nickname:
15794
+ after_job_nickname = item_details.get("xecmpfAfterJobDataId")
15795
+ self.logger.info(
15796
+ "Scheduled bot item -> '%s' starts after another scheduled bot with nickname -> '%s'. Resolving nickname...",
15797
+ item_name,
15798
+ after_job_nickname,
15799
+ )
15800
+ # Get the Scheduled Bot node that this bot depends on:
15801
+ response = self._otcs.get_node_from_nickname(nickname=after_job_nickname)
15802
+ after_job_id = self._otcs.get_result_value(
15803
+ response=response,
15804
+ key="id",
15805
+ )
15806
+ # Update the bot details - changing the name coming from the
15807
+ # payload to the actual node ID (both in details and in 'old' details):
15808
+ scheduled_bot_details["startmodus"]["afterJob"] = after_job_id
15809
+ old_schedule_data["afterJob"] = after_job_id
15810
+
15811
+ item_details["xecmpfAfterJobDataId"] = after_job_id
15812
+ # Check if this bot should start based on a schedule.
15813
+ # Then we need to configure the agent to run it:
15814
+ elif str(start_mode) == "7558": # 7558 is NOT a object ID but an internal agent type ID!
15815
+ self.logger.info(
15816
+ "Scheduled bot item -> '%s' starts based on a schedule. Setting the agent ID to '7558'...",
15817
+ item_name,
15818
+ )
15819
+ # Make sure the agent ID is configured:
15820
+ item_details["xecmpfAgentId"] = 7558
15821
+ self.logger.debug("Scheduled bot details -> %s", str(scheduled_bot_details))
15822
+
15823
+ # The REST API requires to have the scheduled bot details wrapped into a JSON structure:
15205
15824
  item_details["scheduledbotdetails"] = json.dumps(item_details.get("scheduledbotdetails", {}))
15206
15825
 
15207
15826
  response = self._otcs.update_item(node_id=node_id, body=False, **item_details)
15208
15827
  if not response:
15209
- self.logger.error("Failed to update scheduld bot item -> '%s'.", item_name)
15828
+ self.logger.error("Failed to update scheduled bot item -> '%s'.", item_name)
15210
15829
  success = False
15211
15830
  continue
15212
15831
 
15213
- response = self._otcs.update_item(node_id=node_id, body=False, actionName="Runnow")
15214
- if not response:
15215
- self.logger.error("Failed to run scheduld bot item -> '%s'.", item_name)
15216
- success = False
15217
- continue
15832
+ # If the Job has start mode manual we start it now:
15833
+ if start_mode == "manual":
15834
+ self.logger.info("Run scheduled bot -> '%s' now...", item_name)
15835
+ response = self._otcs.update_item(node_id=node_id, body=False, actionName="Runnow")
15836
+ if not response:
15837
+ self.logger.error("Failed to run scheduled bot item -> '%s'.", item_name)
15838
+ success = False
15839
+ continue
15840
+ # end if item_type == self._otcs.ITEM_TYPE_SCHEDULED_BOT:
15218
15841
 
15219
15842
  # Special handling for collection items:
15220
15843
  elif item_type == self._otcs.ITEM_TYPE_COLLECTION:
@@ -15269,6 +15892,11 @@ class Payload:
15269
15892
  node_ids=item_node_ids,
15270
15893
  )
15271
15894
  # end if item_type == self._otcs.ITEM_TYPE_COLLECTION
15895
+
15896
+ # Do we have a nickname for the item in the payload? Then assign it:
15897
+ if item_nickname:
15898
+ self.logger.info("Assign nickname -> '%s' to item -> '%s' (%s)", item_nickname, item_name, node_id)
15899
+ self._otcs.set_node_nickname(node_id=node_id, nickname=item_nickname)
15272
15900
  # end for item in items:
15273
15901
 
15274
15902
  self.write_status_file(
@@ -16081,7 +16709,7 @@ class Payload:
16081
16709
  self,
16082
16710
  resource_name: str,
16083
16711
  license_feature: str,
16084
- license_name: str,
16712
+ license_name: str = "EXTENDED_ECM",
16085
16713
  user_specific_payload_field: str = "licenses",
16086
16714
  section_name: str = "userLicenses",
16087
16715
  ) -> bool:
@@ -16168,9 +16796,42 @@ class Payload:
16168
16796
  user_license_features = [license_feature]
16169
16797
 
16170
16798
  for user_license_feature in user_license_features:
16799
+ if isinstance(user_license_feature, dict):
16800
+ user_license_feature_dict = user_license_feature
16801
+ user_license_feature = user_license_feature_dict.get("feature", license_feature)
16802
+ license_name = user_license_feature_dict.get("name", license_name)
16803
+ if "enabled" in user_license_feature_dict and not user_license_feature_dict["enabled"]:
16804
+ self.logger.info(
16805
+ "Payload for License '%s' -> '%s' is disabled. Skipping...",
16806
+ license_name,
16807
+ license_feature,
16808
+ )
16809
+ continue
16810
+
16811
+ if "resource" in user_license_feature_dict:
16812
+ try:
16813
+ resource_id = self._otds.get_resource(name=user_license_feature_dict["resource"])[
16814
+ "resourceID"
16815
+ ]
16816
+ except Exception:
16817
+ self.logger.error(
16818
+ "Error retrieving resourceID for -> %s", user_license_feature_dict["resource"]
16819
+ )
16820
+ continue
16821
+ success = False
16822
+ else:
16823
+ resource_id = otds_resource["resourceID"]
16824
+
16825
+ elif isinstance(user_license_feature, str):
16826
+ resource_id = otds_resource["resourceID"]
16827
+
16828
+ else:
16829
+ self.logger.error("Invalid License feature specified -> %s", user_license_feature)
16830
+ continue
16831
+
16171
16832
  if self._otds.is_user_licensed(
16172
16833
  user_name=user_name,
16173
- resource_id=otds_resource["resourceID"],
16834
+ resource_id=resource_id,
16174
16835
  license_feature=user_license_feature,
16175
16836
  license_name=license_name,
16176
16837
  ):
@@ -16184,7 +16845,7 @@ class Payload:
16184
16845
  assigned_license = self._otds.assign_user_to_license(
16185
16846
  partition=user_partition,
16186
16847
  user_id=user_name, # we want the plain login name here
16187
- resource_id=otds_resource["resourceID"],
16848
+ resource_id=resource_id,
16188
16849
  license_feature=user_license_feature,
16189
16850
  license_name=license_name,
16190
16851
  )
@@ -16330,6 +16991,8 @@ class Payload:
16330
16991
 
16331
16992
  """
16332
16993
 
16994
+ self.logger.warning("execPodCommand is deprecated - use kubernetes section with action 'execPodCommands'")
16995
+
16333
16996
  if not isinstance(self._k8s, K8s):
16334
16997
  self.logger.error(
16335
16998
  "K8s not setup properly -> Skipping payload section -> '%s'...",
@@ -16370,6 +17033,7 @@ class Payload:
16370
17033
  continue
16371
17034
 
16372
17035
  container = exec_pod_command.get("container", None)
17036
+ timeout = int(exec_pod_command.get("timeout", 60))
16373
17037
 
16374
17038
  # Check if exec pod command has been explicitly disabled in payload
16375
17039
  # (enabled = false). In this case we skip the element:
@@ -16398,17 +17062,9 @@ class Payload:
16398
17062
 
16399
17063
  if "interactive" not in exec_pod_command or exec_pod_command["interactive"] is False:
16400
17064
  result = self._k8s.exec_pod_command(
16401
- pod_name=pod_name,
16402
- command=command,
16403
- container=container,
16404
- )
16405
- elif "timeout" not in exec_pod_command:
16406
- result = self._k8s.exec_pod_command_interactive(
16407
- pod_name=pod_name,
16408
- commands=command,
17065
+ pod_name=pod_name, command=command, container=container, timeout=timeout
16409
17066
  )
16410
17067
  else:
16411
- timeout = exec_pod_command["timeout"]
16412
17068
  result = self._k8s.exec_pod_command_interactive(
16413
17069
  pod_name=pod_name,
16414
17070
  commands=command,
@@ -16421,31 +17077,349 @@ class Payload:
16421
17077
  # 3. result is a non-empty string - this is OK - print it to log
16422
17078
  if result is None:
16423
17079
  self.logger.error(
16424
- "Execution of command -> %s in pod -> '%s' failed",
16425
- command,
16426
- pod_name,
17080
+ "Execution of command -> %s in pod -> '%s' failed",
17081
+ command,
17082
+ pod_name,
17083
+ )
17084
+ success = False
17085
+ elif result != "":
17086
+ self.logger.info(
17087
+ "Execution of command -> %s in pod -> '%s' returned result -> %s",
17088
+ command,
17089
+ pod_name,
17090
+ result,
17091
+ )
17092
+ else:
17093
+ # It is not an error if no result is returned. It depends on the nature of the command
17094
+ # if a result is written to stdout or stderr.
17095
+ self.logger.info(
17096
+ "Execution of command -> %s in pod -> '%s' did not return a result",
17097
+ command,
17098
+ pod_name,
17099
+ )
17100
+
17101
+ self.write_status_file(
17102
+ success=success,
17103
+ payload_section_name=section_name,
17104
+ payload_section=self._exec_pod_commands,
17105
+ )
17106
+
17107
+ return success
17108
+
17109
+ # end method definition
17110
+
17111
+ def process_kubernetes(self, section_name: str = "kubernetes") -> bool:
17112
+ """Process actions that should be executed in the Kubernetess.
17113
+
17114
+ Args:
17115
+ section_name (str, optional):
17116
+ The name of the payload section. It can be overridden
17117
+ for cases where multiple sections of same type
17118
+ are used (e.g. the "Post" sections).
17119
+ This name is also used for the "success" status
17120
+ files written to the Admin Personal Workspace.
17121
+
17122
+ Returns:
17123
+ bool:
17124
+ True if payload has been processed without errors, False otherwise.
17125
+
17126
+ """
17127
+
17128
+ if not isinstance(self._k8s, K8s):
17129
+ self.logger.error(
17130
+ "K8s not setup properly -> Skipping payload section -> '%s'...",
17131
+ section_name,
17132
+ )
17133
+ return False
17134
+
17135
+ if not self._kubernetes:
17136
+ self.logger.info(
17137
+ "Payload section -> '%s' is empty. Skipping...",
17138
+ section_name,
17139
+ )
17140
+ return True
17141
+
17142
+ # If this payload section has been processed successfully before we
17143
+ # can return True and skip processing it once more:
17144
+ if self.check_status_file(payload_section_name=section_name):
17145
+ return True
17146
+
17147
+ success: bool = True
17148
+
17149
+ for item in self._kubernetes:
17150
+ # Check if element has been disabled in payload (enabled = false).
17151
+ # In this case we skip the element:
17152
+ if isinstance(item, dict) and not item.get("enabled", True):
17153
+ self.logger.info("Skipping disabled item -> %s", item)
17154
+ continue
17155
+
17156
+ match item.get("action"):
17157
+ case "execPodCommand":
17158
+ pod_name = item.get("pod_name")
17159
+
17160
+ if not pod_name:
17161
+ self.logger.error(
17162
+ "To execute a command in a pod the pod name needs to be specified in the payload! Skipping to next kubernetes item...",
17163
+ )
17164
+ success = False
17165
+ continue
17166
+
17167
+ command = item.get("command", [])
17168
+ if not command:
17169
+ self.logger.error(
17170
+ "Command is not specified for pod -> %s! It needs to be a non-empty list! Skipping to next kubernetes item...",
17171
+ pod_name,
17172
+ )
17173
+ success = False
17174
+ continue
17175
+
17176
+ container = item.get("container", None)
17177
+ timeout = int(item.get("timeout", 60))
17178
+
17179
+ # Check if exec pod command has been explicitly disabled in payload
17180
+ # (enabled = false). In this case we skip the element:
17181
+ if not item.get("enabled", True):
17182
+ self.logger.info(
17183
+ "Payload for exec pod command in pod -> '%s' is disabled. Skipping...",
17184
+ pod_name,
17185
+ )
17186
+ continue
17187
+
17188
+ if "description" not in item:
17189
+ self.logger.info(
17190
+ "Executing command -> %s in pod -> '%s'",
17191
+ command,
17192
+ pod_name,
17193
+ )
17194
+
17195
+ else:
17196
+ description = item["description"]
17197
+ self.logger.info(
17198
+ "Executing command -> %s in pod -> '%s' (%s)",
17199
+ command,
17200
+ pod_name,
17201
+ description,
17202
+ )
17203
+
17204
+ if "interactive" not in item or item["interactive"] is False:
17205
+ result = self._k8s.exec_pod_command(
17206
+ pod_name=pod_name, command=command, container=container, timeout=timeout
17207
+ )
17208
+ else:
17209
+ result = self._k8s.exec_pod_command_interactive(
17210
+ pod_name=pod_name,
17211
+ commands=command,
17212
+ timeout=timeout,
17213
+ )
17214
+
17215
+ # we need to differentiate 3 cases here:
17216
+ # 1. result = None is returned - this is an error (exception)
17217
+ # 2. result is empty string - this is OK
17218
+ # 3. result is a non-empty string - this is OK - print it to log
17219
+ if result is None:
17220
+ self.logger.error(
17221
+ "Execution of command -> %s in pod -> '%s' failed",
17222
+ command,
17223
+ pod_name,
17224
+ )
17225
+ success = False
17226
+ elif result != "":
17227
+ self.logger.info(
17228
+ "Execution of command -> %s in pod -> '%s' returned result -> %s",
17229
+ command,
17230
+ pod_name,
17231
+ result,
17232
+ )
17233
+ else:
17234
+ # It is not an error if no result is returned. It depends on the nature of the command
17235
+ # if a result is written to stdout or stderr.
17236
+ self.logger.info(
17237
+ "Execution of command -> %s in pod -> '%s' did not return a result",
17238
+ command,
17239
+ pod_name,
17240
+ )
17241
+
17242
+ case "restart":
17243
+ k8s_type = item.get("type")
17244
+ name = item.get("name")
17245
+ message = item.get("message")
17246
+
17247
+ if not name:
17248
+ self.logger.error("Name not specified for kubernetes item -> %s", item)
17249
+ if not k8s_type:
17250
+ self.logger.error("Type not specified for kubernetes item -> %s", item)
17251
+
17252
+ if message:
17253
+ self.logger.info("%s", message)
17254
+
17255
+ if k8s_type.lower() == "statefulset":
17256
+ self.logger.info("Restarting statefulset -> %s", name)
17257
+ restart_result = self._k8s.restart_stateful_set(sts_name=name)
17258
+
17259
+ elif k8s_type.lower() == "deployment":
17260
+ self.logger.info("Restarting deployment -> %s", name)
17261
+ restart_result = self._k8s.restart_deployment(deployment_name=name)
17262
+
17263
+ elif k8s_type.lower() == "pod":
17264
+ self.logger.info("Deleting pod -> %s", name)
17265
+ restart_result = self._k8s.delete_pod(pod_name=name)
17266
+
17267
+ if not restart_result:
17268
+ success = False
17269
+
17270
+ case _:
17271
+ self.logger.error("Action not defined for work item -> %s", item)
17272
+ continue
17273
+
17274
+ self.write_status_file(
17275
+ success=success,
17276
+ payload_section_name=section_name,
17277
+ payload_section=self._kubernetes,
17278
+ )
17279
+
17280
+ return success
17281
+
17282
+ # end method definition
17283
+
17284
+ def process_exec_database_commands(self, section_name: str = "execDatabaseCommands") -> bool:
17285
+ """Process commands that should be executed in the PostgreSQL database.
17286
+
17287
+ Args:
17288
+ section_name (str, optional):
17289
+ The name of the payload section. It can be overridden
17290
+ for cases where multiple sections of same type
17291
+ are used (e.g. the "Post" sections).
17292
+ This name is also used for the "success" status
17293
+ files written to the Admin Personal Workspace.
17294
+
17295
+ Returns:
17296
+ bool:
17297
+ True if payload has been processed without errors, False otherwise.
17298
+
17299
+ """
17300
+
17301
+ if not self._exec_database_commands:
17302
+ self.logger.info(
17303
+ "Payload section -> '%s' is empty. Skipping...",
17304
+ section_name,
17305
+ )
17306
+ return True
17307
+
17308
+ if not psycopg_installed:
17309
+ self.logger.warning("Python module 'psycopg' not installed. Cannot execute database commands. Skipping...")
17310
+ return False
17311
+
17312
+ # If this payload section has been processed successfully before we
17313
+ # can return True and skip processing it once more:
17314
+ if self.check_status_file(payload_section_name=section_name):
17315
+ return True
17316
+
17317
+ success: bool = True
17318
+
17319
+ for database_command_set in self._exec_database_commands:
17320
+ # Check if exec pod command has been explicitly disabled in payload
17321
+ # (enabled = false). In this case we skip the element:
17322
+ if not database_command_set.get("enabled", True):
17323
+ self.logger.info(
17324
+ "Payload for database command set is disabled. Skipping...",
17325
+ )
17326
+ continue
17327
+
17328
+ db_connection = database_command_set.get("db_connection")
17329
+ if not db_connection:
17330
+ self.logger.error(
17331
+ "To execute a command in a database the connection information needs to be specified in the payload! Skipping to next database command set...",
17332
+ )
17333
+ success = False
17334
+ continue
17335
+
17336
+ db_name = db_connection.get("db_name", None)
17337
+ if not db_name:
17338
+ self.logger.error(
17339
+ "Database connection information is missing the database name! Skipping to next database command set...",
17340
+ )
17341
+ success = False
17342
+ continue
17343
+ db_hostname = db_connection.get("db_hostname", None)
17344
+ if not db_hostname:
17345
+ self.logger.error(
17346
+ "Database connection information is missing the database hostname! Skipping to next database command set...",
16427
17347
  )
16428
17348
  success = False
16429
- elif result != "":
16430
- self.logger.info(
16431
- "Execution of command -> %s in pod -> '%s' returned result -> %s",
16432
- command,
16433
- pod_name,
16434
- result,
17349
+ continue
17350
+ db_port = int(db_connection.get("db_port", 5432))
17351
+ db_username = db_connection.get("db_username", None)
17352
+ if not db_username:
17353
+ self.logger.error(
17354
+ "Database connection information is missing the database username! Skipping to next database command set...",
16435
17355
  )
16436
- else:
16437
- # It is not an error if no result is returned. It depends on the nature of the command
16438
- # if a result is written to stdout or stderr.
16439
- self.logger.info(
16440
- "Execution of command -> %s in pod -> '%s' did not return a result",
16441
- command,
16442
- pod_name,
17356
+ success = False
17357
+ continue
17358
+ db_password = db_connection.get("db_password", None)
17359
+ if not db_password:
17360
+ self.logger.error(
17361
+ "Database connection information is missing the database password! Skipping to next database command set...",
16443
17362
  )
17363
+ success = False
17364
+ continue
17365
+
17366
+ connect_string = "dbname={} user={} password={} host={} port={}".format(
17367
+ db_name, db_username, db_password, db_hostname, db_port
17368
+ )
17369
+
17370
+ db_commands = database_command_set.get("db_commands", [])
17371
+
17372
+ # TODO: Add support for sql file
17373
+
17374
+ db_connection = None # Predefine for safe access in except
17375
+ allowed_verbs = {"SELECT", "INSERT", "UPDATE", "CREATE"}
17376
+
17377
+ try:
17378
+ # Using a context managers (with ...) for automatic resource management:
17379
+ with psycopg.connect(connect_string) as db_connection:
17380
+ self.logger.info(
17381
+ "Connected to database -> '%s' (%s) with user -> '%s'", db_name, db_hostname, db_username
17382
+ )
17383
+ with db_connection.cursor() as cursor:
17384
+ for db_command in db_commands:
17385
+ cmd = db_command.get("command", None)
17386
+ if not cmd:
17387
+ self.logger.warning(
17388
+ "Cannot execute database command without SQL statement. Skipping..."
17389
+ )
17390
+ continue
17391
+ params = db_command.get("params", None)
17392
+ if params is not None and isinstance(params, (list, tuple)):
17393
+ self.logger.error(
17394
+ "Database parameters -> %s must be given as a list or tuple!", str(params)
17395
+ )
17396
+ continue
17397
+ # Get the command verb (like "SELECT", "CREATE")
17398
+ verb = cmd.strip().split()[0].upper()
17399
+ if verb not in allowed_verbs:
17400
+ self.logger.error("Database command -> '%s' is not allowed!", verb)
17401
+ continue
17402
+ if params:
17403
+ self.logger.info(
17404
+ "Execute database command -> '%s' with parameters -> %s...", cmd, str(params)
17405
+ )
17406
+ else:
17407
+ self.logger.info("Execute database command -> '%s' without parameters...", cmd)
17408
+ cursor.execute(cmd, params)
17409
+ if verb == "SELECT":
17410
+ response = cursor.fetchall()
17411
+ self.logger.debug("Database response -> '%s'", response)
17412
+ db_connection.commit()
17413
+ except psycopg.Error as e:
17414
+ success = False
17415
+ self.logger.error("Database error -> %s", e)
17416
+ if db_connection is not None:
17417
+ db_connection.rollback()
16444
17418
 
16445
17419
  self.write_status_file(
16446
17420
  success=success,
16447
17421
  payload_section_name=section_name,
16448
- payload_section=self._exec_pod_commands,
17422
+ payload_section=self._exec_database_commands,
16449
17423
  )
16450
17424
 
16451
17425
  return success
@@ -16467,7 +17441,7 @@ class Payload:
16467
17441
  files written to the Admin Personal Workspace.
16468
17442
 
16469
17443
  Returns:
16470
- bool:
17444
+ bool:,
16471
17445
  True if payload has been processed without errors, False otherwise.
16472
17446
 
16473
17447
  """
@@ -16659,7 +17633,7 @@ class Payload:
16659
17633
  authenticated_user = exec_as_user
16660
17634
  else:
16661
17635
  self.logger.error(
16662
- "Cannot find user with login name -> '%s' for executing. Executing as admin...",
17636
+ "Cannot find user with login name -> '%s' for executing document generator. Executing as admin...",
16663
17637
  exec_as_user,
16664
17638
  )
16665
17639
  admin_context = True
@@ -17250,7 +18224,7 @@ class Payload:
17250
18224
  section_name: str = "browserAutomations",
17251
18225
  check_status: bool = True,
17252
18226
  ) -> bool:
17253
- """Process Selenium-based browser automations.
18227
+ """Process Playwright-based browser automations and tests.
17254
18228
 
17255
18229
  Args:
17256
18230
  browser_automations (list):
@@ -17288,51 +18262,60 @@ class Payload:
17288
18262
 
17289
18263
  success: bool = True
17290
18264
 
18265
+ automation_type = "Browser" if "browser" in section_name else "Test"
18266
+
17291
18267
  for browser_automation in browser_automations:
17292
18268
  name = browser_automation.get("name")
17293
18269
  if not name:
17294
- self.logger.error(
17295
- "Browser automation is missing a unique name. Skipping...",
17296
- )
18270
+ self.logger.error("%s automation is missing a unique name. Skipping...", automation_type)
17297
18271
  success = False
17298
18272
  continue
17299
18273
 
18274
+ self._log_header_callback(
18275
+ text="Process {} Automation -> '{}'".format(automation_type, name),
18276
+ char="-",
18277
+ )
18278
+
17300
18279
  description = browser_automation.get("description", "")
18280
+ if description:
18281
+ self.logger.info(
18282
+ "%s Automation description -> '%s'",
18283
+ automation_type,
18284
+ description,
18285
+ )
17301
18286
 
17302
18287
  # Check if browser automation has been explicitly disabled in payload
17303
18288
  # (enabled = false). In this case we skip this payload element:
17304
18289
  if not browser_automation.get("enabled", True):
17305
18290
  self.logger.info(
17306
- "Payload for browser automation -> '%s'%s is disabled. Skipping...",
18291
+ "Payload for %s automation -> '%s'%s is disabled. Skipping...",
18292
+ automation_type.lower(),
17307
18293
  name,
17308
18294
  " ({})".format(description) if description else "",
17309
18295
  )
17310
18296
  continue
17311
18297
 
17312
- self.logger.info(
17313
- "Processing Browser Automation -> '%s'%s...",
17314
- name,
17315
- " ({})".format(description) if description else "",
17316
- )
17317
-
17318
18298
  base_url = browser_automation.get("base_url")
17319
18299
  if not base_url:
17320
- self.logger.error("Browser automation -> '%s' is missing base_url. Skipping...", name)
18300
+ self.logger.error(
18301
+ "%s automation -> '%s' is missing 'base_url' parameter. Skipping...", automation_type, name
18302
+ )
17321
18303
  success = False
17322
18304
  continue
17323
18305
 
17324
18306
  user_name = browser_automation.get("user_name", "")
17325
18307
  if not user_name:
17326
- self.logger.info("Browser automation -> '%s' is not having user name.", name)
18308
+ self.logger.info("%s automation -> '%s' is not having user name.", automation_type, name)
17327
18309
 
17328
18310
  password = browser_automation.get("password", "")
17329
18311
  if not password:
17330
- self.logger.info("Browser automation -> '%s' is not having password.", name)
18312
+ self.logger.info("%s automation -> '%s' is not having password.", automation_type, name)
17331
18313
 
17332
- automations = browser_automation.get("automations", [])
17333
- if not automations:
18314
+ automation_steps = browser_automation.get("automations", [])
18315
+ if not automation_steps:
17334
18316
  self.logger.error(
17335
- "Browser automation -> '%s' is missing list of automations. Skipping...",
18317
+ "%s automation -> '%s' is missing list of automations. Skipping...",
18318
+ automation_type,
17336
18319
  name,
17337
18320
  )
17338
18321
  success = False
@@ -17341,162 +18324,335 @@ class Payload:
17341
18324
  debug_automation: bool = browser_automation.get("debug", False)
17342
18325
 
17343
18326
  # Create Selenium Browser Automation:
17344
- self.logger.info("Browser Automation base URL -> %s", base_url)
17345
- self.logger.info("Browser Automation user -> %s", user_name)
17346
- self.logger.debug("Browser Automation password -> %s", password)
18327
+ self.logger.info("%s Automation base URL -> %s", automation_type, base_url)
18328
+ self.logger.info("%s Automation user -> '%s'", automation_type, user_name)
18329
+ wait_until = browser_automation.get("wait_until") # it is OK to be None
18330
+ if "wait_until" in browser_automation:
18331
+ # Only log the "wait until" value if it is specified in the payload:
18332
+ self.logger.info(
18333
+ "%s Automation page navigation strategy is to wait until -> '%s'.",
18334
+ automation_type,
18335
+ wait_until,
18336
+ )
17347
18337
  browser_automation_object = BrowserAutomation(
17348
18338
  base_url=base_url,
17349
18339
  user_name=user_name,
17350
18340
  user_password=password,
17351
18341
  automation_name=name,
17352
18342
  take_screenshots=debug_automation,
18343
+ headless=self._browser_headless,
17353
18344
  logger=self.logger,
18345
+ wait_until=wait_until,
17354
18346
  )
17355
- # Implicit Wait is a global setting (for whole brwoser session)
18347
+ # Wait time is a global setting (for whole brwoser session)
17356
18348
  # This makes sure a page is fully loaded and elements are present
17357
18349
  # before accessing them. We set 15.0 seconds as default if not
17358
18350
  # otherwise specified by "wait_time" in the payload.
17359
- # See https://www.selenium.dev/documentation/webdriver/waits/
17360
18351
  wait_time = browser_automation.get("wait_time", 15.0)
17361
- browser_automation_object.implicit_wait(wait_time)
18352
+ browser_automation_object.set_timeout(wait_time=wait_time)
17362
18353
  if "wait_time" in browser_automation:
17363
18354
  self.logger.info(
17364
- "Browser Automation Implicit Wait time -> '%s' configured",
18355
+ "%s Automation wait time -> '%s' configured.",
18356
+ automation_type,
17365
18357
  wait_time,
17366
18358
  )
17367
18359
 
17368
- for automation in automations:
17369
- if "type" not in automation:
17370
- self.logger.error(
17371
- "Browser automation step is missing type. Skipping...",
17372
- )
18360
+ # Initialize overall result status:
18361
+ result = True
18362
+ first_step = True
18363
+
18364
+ for automation_step in automation_steps:
18365
+ if "type" not in automation_step:
18366
+ self.logger.error("%s automation step is missing type. Skipping...", automation_type)
17373
18367
  success = False
17374
18368
  break
17375
- automation_type = automation.get("type", "")
18369
+ automation_step_type = automation_step.get("type", "")
18370
+ dependent = automation_step.get("dependent", True)
18371
+ if not dependent and not result:
18372
+ self.logger.warning(
18373
+ "Ignore result of previous step as current step -> '%s' is NOT dependent on it.",
18374
+ automation_step_type,
18375
+ )
18376
+ result = True
18377
+ elif not result:
18378
+ # In this case a proceeding automation step has failed
18379
+ # and this step is marked as dependent. Then it does not make sense
18380
+ # to continue with this automation step after the proceeding step failed.
18381
+ self.logger.warning(
18382
+ "Step -> '%s' is dependent on a proceeding step that failed. Skipping this step...",
18383
+ automation_step_type,
18384
+ )
18385
+ continue
18386
+ elif not first_step:
18387
+ self.logger.info(
18388
+ "Current step -> '%s' is %s on proceeding step.",
18389
+ automation_step_type,
18390
+ "dependent" if dependent else "not dependent",
18391
+ )
17376
18392
 
17377
- match automation_type:
18393
+ match automation_step_type:
17378
18394
  case "login":
17379
- page = automation.get("page", "")
18395
+ page = automation_step.get("page", "")
18396
+ user_field = automation_step.get("user_field", "otds_username")
18397
+ password_field = automation_step.get(
18398
+ "password_field",
18399
+ "otds_password",
18400
+ )
18401
+ login_button = automation_step.get("login_button", "loginbutton")
18402
+ # Do we have a step-specific wait mechanism? If not, we pass None
18403
+ # then the browser automation will take the default configured for
18404
+ # the whole browser automation (see BrowserAutomation() constructor above):
18405
+ wait_until = automation_step.get("wait_until", None)
17380
18406
  self.logger.info(
17381
- "Login to -> %s as user -> %s",
18407
+ "Login to -> %s as user -> '%s' (%s page navigation strategy is to wait until -> '%s')",
17382
18408
  base_url + page,
17383
18409
  user_name,
18410
+ "specific" if wait_until is not None else "default",
18411
+ wait_until if wait_until is not None else browser_automation_object.wait_until,
17384
18412
  )
17385
- user_field = automation.get("user_field", "otds_username")
17386
- password_field = automation.get(
17387
- "password_field",
17388
- "otds_password",
17389
- )
17390
- login_button = automation.get("login_button", "loginbutton")
17391
- if not browser_automation_object.run_login(
18413
+ result = browser_automation_object.run_login(
17392
18414
  page=page,
17393
18415
  user_field=user_field,
17394
18416
  password_field=password_field,
17395
18417
  login_button=login_button,
17396
- ):
18418
+ wait_until=wait_until,
18419
+ )
18420
+ if not result:
17397
18421
  self.logger.error(
17398
- "Cannot log into -> %s. Stopping automation.",
18422
+ "Cannot log into -> %s. Skipping to next automation step...",
17399
18423
  base_url + page,
17400
18424
  )
17401
18425
  success = False
17402
- break
18426
+ continue
17403
18427
  self.logger.info(
17404
- "Successfully logged into page -> %s.",
18428
+ "Successfully logged into page -> %s. Page title -> '%s'.",
17405
18429
  base_url + page,
18430
+ browser_automation_object.get_title(),
17406
18431
  )
17407
18432
  case "get_page":
17408
- page = automation.get("page", "")
18433
+ page = automation_step.get("page", "")
17409
18434
  if not page:
17410
18435
  self.logger.error(
17411
- "Automation type -> '%s' requires page parameter. Stopping automation.",
17412
- automation_type,
18436
+ "Automation step type -> '%s' requires 'page' parameter. Stopping automation.",
18437
+ automation_step_type,
17413
18438
  )
17414
18439
  success = False
17415
18440
  break
17416
- self.logger.info("Get page -> %s", base_url + page)
17417
- if not browser_automation_object.get_page(url=page):
18441
+ volume = automation_step.get("volume", OTCS.VOLUME_TYPE_ENTERPRISE_WORKSPACE)
18442
+ path = automation_step.get("path", [])
18443
+ if path and volume:
18444
+ page_node = self._otcs.get_node_by_volume_and_path(
18445
+ volume_type=volume,
18446
+ path=path,
18447
+ create_path=False,
18448
+ )
18449
+ page_id = self._otcs.get_result_value(response=page_node, key="id")
18450
+ if not page_id:
18451
+ # if not parent_node:
18452
+ self.logger.error(
18453
+ "%s automation -> '%s' has a page path that does not exist. Skipping...",
18454
+ automation_type,
18455
+ name,
18456
+ )
18457
+ success = False
18458
+ continue
18459
+ self.logger.info(
18460
+ "Resolved volume -> %d and page path -> %s to node ID -> %d", volume, path, page_id
18461
+ )
18462
+ else:
18463
+ page_id = None
18464
+ if "{}" in page and page_id:
18465
+ page = page.format(page_id)
18466
+ # Do we have a step-specific wait mechanism? If not, we pass None
18467
+ # then the browser automation will take the default configured for
18468
+ # the whole browser automation (see BrowserAutomation() constructor called above):
18469
+ wait_until = automation_step.get("wait_until", None)
18470
+ self.logger.info(
18471
+ "Load page -> %s (%s page navigation strategy is to wait until -> '%s')",
18472
+ base_url + page,
18473
+ "specific" if wait_until is not None else "default",
18474
+ wait_until if wait_until is not None else browser_automation_object.wait_until,
18475
+ )
18476
+ result = browser_automation_object.get_page(url=page, wait_until=wait_until)
18477
+ if not result:
17418
18478
  self.logger.error(
17419
- "Cannot get page -> %s. Stopping automation.",
18479
+ "Cannot load page -> %s. Skipping this step...",
17420
18480
  page,
17421
18481
  )
17422
18482
  success = False
17423
- break
18483
+ continue
17424
18484
  self.logger.info(
17425
- "Successfully loaded page -> %s",
18485
+ "Successfully loaded page -> %s. Page title -> '%s'.",
17426
18486
  base_url + page,
18487
+ browser_automation_object.get_title(),
17427
18488
  )
17428
18489
  case "click_elem":
17429
- elem = automation.get("elem", "")
17430
- if not elem:
18490
+ # We keep the deprecated "elem" syntax supported (for now)
18491
+ selector = automation_step.get("selector", automation_step.get("elem", ""))
18492
+ if not selector:
17431
18493
  self.logger.error(
17432
- "Automation type -> '%s' requires elem parameter. Stopping automation.",
17433
- automation_type,
18494
+ "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18495
+ automation_step_type,
17434
18496
  )
17435
18497
  success = False
17436
18498
  break
17437
- find = automation.get("find", "id")
17438
- show_error = automation.get("show_error", True)
17439
- if not browser_automation_object.find_elem_and_click(
17440
- find_elem=elem,
17441
- find_method=find,
18499
+ # We keep the deprecated "find" syntax supported (for now)
18500
+ selector_type = automation_step.get("selector_type", automation_step.get("find", "id"))
18501
+ show_error = automation_step.get("show_error", True)
18502
+ navigation = automation_step.get("navigation", False)
18503
+ checkbox_state = automation_step.get("checkbox_state", None)
18504
+ # Do we have a step-specific wait mechanism? If not, we pass None
18505
+ # then the browser automation will take the default configured for
18506
+ # the whole browser automation (see BrowserAutomation() constructor called above):
18507
+ wait_until = automation_step.get("wait_until", None)
18508
+ role_type = automation_step.get("role_type", None)
18509
+ result = browser_automation_object.find_elem_and_click(
18510
+ selector=selector,
18511
+ selector_type=selector_type,
18512
+ role_type=role_type,
18513
+ desired_checkbox_state=checkbox_state,
18514
+ is_navigation_trigger=navigation,
18515
+ wait_until=wait_until,
17442
18516
  show_error=show_error,
17443
- ):
17444
- self.logger.error(
17445
- "Cannot find clickable element -> '%s' on current page. Stopping automation.",
17446
- elem,
18517
+ )
18518
+ if not result:
18519
+ message = "Cannot find clickable element with selector -> '{}' ({}) on current page. Skipping this step...".format(
18520
+ selector, selector_type
17447
18521
  )
17448
- success = False
17449
- break
18522
+ if show_error:
18523
+ self.logger.error(message)
18524
+ success = False
18525
+ else:
18526
+ self.logger.warning(message)
18527
+ continue
17450
18528
  self.logger.info(
17451
- "Successfully clicked element -> '%s'",
17452
- elem,
18529
+ "Successfully clicked %s element selected by -> '%s' (%s)",
18530
+ "navigational" if navigation else "non-navigational",
18531
+ selector,
18532
+ selector_type,
17453
18533
  )
17454
18534
  case "set_elem":
17455
- elem = automation.get("elem", "")
17456
- if not elem:
18535
+ # We keep the deprecated "elem" syntax supported (for now)
18536
+ selector = automation_step.get("selector", automation_step.get("elem", ""))
18537
+ if not selector:
17457
18538
  self.logger.error(
17458
- "Automation type -> '%s' requires elem parameter",
17459
- automation_type,
18539
+ "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18540
+ automation_step_type,
17460
18541
  )
17461
18542
  success = False
17462
18543
  break
17463
- find = automation.get("find", "id")
17464
- value = automation.get("value", "")
18544
+ # We keep the deprecated "find" syntax supported (for now)
18545
+ selector_type = automation_step.get("selector_type", automation_step.get("find", "id"))
18546
+ role_type = automation_step.get("role_type", None)
18547
+ value = automation_step.get("value", "")
17465
18548
  if not value:
17466
18549
  self.logger.error(
17467
- "Automation of type -> '%s' for element -> '%s' requires value parameter. Stopping automation.",
17468
- automation_type,
17469
- elem,
18550
+ "Automation step type -> '%s' for element selected by -> '%s' (%s) requires 'value' parameter. Stopping automation.",
18551
+ automation_step_type,
18552
+ selector,
18553
+ selector_type,
17470
18554
  )
17471
18555
  success = False
17472
18556
  break
17473
18557
  # we also support replacing placeholders that are
17474
18558
  # enclosed in double % characters like %%OTCS_RESOURCE_ID%%:
17475
18559
  value = self.replace_placeholders(value)
17476
- if not browser_automation_object.find_elem_and_set(
17477
- find_elem=elem,
17478
- elem_value=value,
17479
- find_method=find,
17480
- ):
18560
+ show_error = automation_step.get("show_error", True)
18561
+ result = browser_automation_object.find_elem_and_set(
18562
+ selector=selector,
18563
+ selector_type=selector_type,
18564
+ role_type=role_type,
18565
+ value=value,
18566
+ show_error=show_error,
18567
+ )
18568
+ if not result:
18569
+ message = "Cannot set element selected by -> '{}' ({}) to value -> '{}'. Skipping this step...".format(
18570
+ selector, selector_type, value
18571
+ )
18572
+ if show_error:
18573
+ self.logger.error(message)
18574
+ success = False
18575
+ else:
18576
+ self.logger.warning(message)
18577
+ continue
18578
+ self.logger.info(
18579
+ "Successfully set element selected by -> '%s' (%s) to value -> '%s'.",
18580
+ selector,
18581
+ selector_type,
18582
+ value,
18583
+ )
18584
+ case "check_elem":
18585
+ # We keep the deprecated "elem" syntax supported (for now)
18586
+ selector = automation_step.get("selector", automation_step.get("elem", ""))
18587
+ if not selector:
17481
18588
  self.logger.error(
17482
- "Cannot find element -> '%s' on current page to set value -> '%s'. Stopping automation.",
17483
- elem,
17484
- value,
18589
+ "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18590
+ automation_step_type,
17485
18591
  )
17486
18592
  success = False
17487
18593
  break
18594
+ # We keep the deprecated "find" syntax supported (for now)
18595
+ selector_type = automation_step.get("selector_type", automation_step.get("find", "id"))
18596
+ role_type = automation_step.get("role_type", None)
18597
+ value = automation_step.get("value", None)
18598
+ attribute = automation_step.get("attribute", "")
18599
+ substring = automation_step.get("substring", False)
18600
+ min_count = automation_step.get("min_count", 1)
18601
+ want_exist = automation_step.get("want_exist", True)
18602
+ wait_time = automation_step.get("wait_time", 0.0)
18603
+ if value:
18604
+ # we also support replacing placeholders that are
18605
+ # enclosed in double % characters like %%OTCS_RESOURCE_ID%%:
18606
+ value = self.replace_placeholders(value)
18607
+ (result, count) = browser_automation_object.check_elems_exist(
18608
+ selector=selector,
18609
+ selector_type=selector_type,
18610
+ role_type=role_type,
18611
+ value=value,
18612
+ attribute=attribute,
18613
+ substring=substring,
18614
+ min_count=min_count,
18615
+ wait_time=wait_time, # time to wait before the check is actually done
18616
+ )
18617
+ # Check if we didn't get what we want:
18618
+ if (not result and want_exist) or (result and not want_exist):
18619
+ self.logger.error(
18620
+ "%s %s%s%s on current page. Test failed.%s",
18621
+ "Cannot find" if not result else "Found",
18622
+ "{} elements with selector -> '{}' ({})".format(min_count, selector, selector_type)
18623
+ if min_count > 1
18624
+ else "an element with selector -> '{}' ({})".format(selector, selector_type),
18625
+ " with {}value -> '{}'".format("substring-" if substring else "", value)
18626
+ if value
18627
+ else "",
18628
+ " in attribute -> '{}'".format(attribute) if attribute else "",
18629
+ " Found {}{} occurences.".format(count, " undesirable" if not want_exist else ""),
18630
+ )
18631
+ success = False
18632
+ continue
18633
+ # Don't break here! We want to do all existance tests!
17488
18634
  self.logger.info(
17489
- "Successfully set element -> '%s' to value -> '%s'.",
17490
- elem,
17491
- value,
18635
+ "Successfully passed %sexistence test for %s%s%s on current page.",
18636
+ "non-" if not want_exist else "",
18637
+ "{} elements with selector -> '{}' ({})".format(min_count, selector, selector_type)
18638
+ if min_count > 1
18639
+ else "an element with selector -> '{}' ({})".format(selector, selector_type),
18640
+ " with {}value -> '{}'".format("substring-" if substring else "", value) if value else "",
18641
+ " in attribute -> '{}'".format(attribute) if attribute else "",
17492
18642
  )
17493
18643
  case _:
17494
18644
  self.logger.error(
17495
- "Illegal automation step type -> '%s' in browser automation!",
17496
- automation_type,
18645
+ "Illegal automation step type -> '%s' in %s automation!",
18646
+ automation_step_type,
18647
+ automation_type.lower(),
17497
18648
  )
17498
18649
  success = False
17499
18650
  break
18651
+ # end match automation_step_type:
18652
+ first_step = False
18653
+ # end for automation_step in automation_steps:
18654
+ browser_automation_object.end_session()
18655
+ # end for browser_automation in browser_automations:
17500
18656
 
17501
18657
  if check_status:
17502
18658
  self.write_status_file(
@@ -18097,6 +19253,13 @@ class Payload:
18097
19253
  "otcs_root_node_ids",
18098
19254
  data_source.get("otcs_root_node_id"),
18099
19255
  )
19256
+
19257
+ if not otcs_root_node_ids:
19258
+ self.logger.error(
19259
+ "Content Server root node ID(s) for traversal are missing in payload of bulk data source. Cannot load data!",
19260
+ )
19261
+ return None
19262
+
18100
19263
  # Filter workspace by depth under the given root (only consider items as workspace if they have the right depth in the hierarchy):
18101
19264
  otcs_filter_workspace_depth = data_source.get("otcs_filter_workspace_depth", 0)
18102
19265
  # Filter workspace by subtype (only consider items as workspace if they have the right technical subtype):
@@ -18134,16 +19297,18 @@ class Payload:
18134
19297
  # List of node IDs to exclude in traversing the folders in the OTCS data source:
18135
19298
  otcs_exclude_node_ids = data_source.get("otcs_exclude_node_ids")
18136
19299
 
18137
- if not otcs_root_node_ids:
18138
- self.logger.error(
18139
- "Content Server root node IDs for traversal are missing in payload of bulk data source. Cannot load data!",
18140
- )
18141
- return None
18142
-
18143
19300
  # Document handling parameters:
18144
19301
  otcs_download_documents = data_source.get("otcs_download_documents", True)
18145
19302
  otcs_skip_existing_downloads = data_source.get("otcs_skip_existing_downloads", True)
18146
19303
  otcs_extract_zip = data_source.get("extract_zip", False)
19304
+ # The following parameter controls how column names are constructed. If it is true, then
19305
+ # attribute columns for workspaces and items will use the category ID in the column name.
19306
+ # Wokspace attributes always start with "workspace_cat_". Item attributes start with item_cat_".
19307
+ # If the value of 'otcs_use_numeric_category_identifier' is False then the category name
19308
+ # is converted to lower-case and spaces and non-alphanumeric characters are replaced with "_".
19309
+ # Example with otcs_use_numeric_category_identifier = True: workspace_cat_47110815_10
19310
+ # Example with otcs_use_numeric_category_identifier = False: workspace_cat_customer_use_case_10
19311
+ otcs_use_numeric_category_identifier = data_source.get("otcs_use_numeric_category_identifier", True)
18147
19312
 
18148
19313
  # Ensure Root_node_id is a list of integers
18149
19314
  if not isinstance(otcs_root_node_ids, list):
@@ -18160,6 +19325,7 @@ class Payload:
18160
19325
  password=otcs_password,
18161
19326
  thread_number=otcs_thread_number,
18162
19327
  download_dir=otcs_download_dir,
19328
+ use_numeric_category_identifier=otcs_use_numeric_category_identifier,
18163
19329
  logger=self.logger,
18164
19330
  )
18165
19331
 
@@ -19907,8 +21073,16 @@ class Payload:
19907
21073
  is_list_in_string = False
19908
21074
 
19909
21075
  # The data source loader may have written a real python list into the value
19910
- # In this case the value includes square brackets [...]
19911
- if value.startswith("[") and value.endswith("]"):
21076
+ # In this case the string value includes square brackets [...]
21077
+ # We only do this if the actual type of the value is string and the
21078
+ # proposed value (value_type) is not string:
21079
+ if (
21080
+ isinstance(value, str) # it is a string
21081
+ and value.startswith("[") # it starts with a list indicator character
21082
+ and value.endswith("]") # it ends with a list indicator character
21083
+ and category_item.get("value_type", "")
21084
+ != "string" # it is NOT explicitly stated it should remain a string
21085
+ ):
19912
21086
  # Remove the square brackets and declare it is a list!
19913
21087
  try:
19914
21088
  value = literal_eval(value)
@@ -20995,7 +22169,7 @@ class Payload:
20995
22169
  workspace_id,
20996
22170
  )
20997
22171
 
20998
- self._otcs_frontend.feme_embedd_metadata(
22172
+ self._otcs_frontend.aviator_embed_metadata(
20999
22173
  node_id=workspace_id,
21000
22174
  wait_for_completion=False,
21001
22175
  )
@@ -21137,7 +22311,7 @@ class Payload:
21137
22311
  workspace_id,
21138
22312
  )
21139
22313
 
21140
- self._otcs_frontend.feme_embedd_metadata(
22314
+ self._otcs_frontend.aviator_embed_metadata(
21141
22315
  node_id=workspace_id,
21142
22316
  wait_for_completion=False,
21143
22317
  )
@@ -23032,7 +24206,7 @@ class Payload:
23032
24206
  operations = bulk_document.get("operations", ["create"])
23033
24207
 
23034
24208
  self.logger.info(
23035
- "Bulk create documents (name field -> %s. Operations -> %s.)",
24209
+ "Bulk create documents (name field -> '%s', operations -> %s)",
23036
24210
  name_field,
23037
24211
  str(operations),
23038
24212
  )
@@ -24860,7 +26034,7 @@ class Payload:
24860
26034
  document_id,
24861
26035
  )
24862
26036
 
24863
- self._otcs_frontend.feme_embedd_metadata(
26037
+ self._otcs_frontend.aviator_embed_metadata(
24864
26038
  node_id=document_id,
24865
26039
  workspace_metadata=False,
24866
26040
  document_metadata=aviator_metadata,
@@ -25001,7 +26175,7 @@ class Payload:
25001
26175
  document_id,
25002
26176
  )
25003
26177
 
25004
- self._otcs_frontend.feme_embedd_metadata(
26178
+ self._otcs_frontend.aviator_embed_metadata(
25005
26179
  node_id=document_id,
25006
26180
  workspace_metadata=False,
25007
26181
  document_metadata=aviator_metadata,
@@ -26441,7 +27615,7 @@ class Payload:
26441
27615
 
26442
27616
  # end method definition
26443
27617
 
26444
- def process_feme(self, section_name: str = "feme") -> bool:
27618
+ def process_avts_questions(self, section_name: str = "avtsQuestions") -> bool:
26445
27619
  """Process Aviator Search repositories.
26446
27620
 
26447
27621
  Args:
@@ -26458,7 +27632,69 @@ class Payload:
26458
27632
 
26459
27633
  """
26460
27634
 
26461
- if not self._feme:
27635
+ if not self._avts_questions:
27636
+ self.logger.info(
27637
+ "Payload section -> '%s' is empty. Skipping...",
27638
+ section_name,
27639
+ )
27640
+ return True
27641
+
27642
+ # If this payload section has been processed successfully before we
27643
+ # can return True and skip processing it once more:
27644
+ if self.check_status_file(payload_section_name=section_name):
27645
+ return True
27646
+
27647
+ success: bool = True
27648
+
27649
+ self._avts.authenticate()
27650
+
27651
+ if not self._avts_questions.get("enabled", True):
27652
+ self.logger.info(
27653
+ "Payload section -> '%s' is not enabled. Skipping...",
27654
+ section_name,
27655
+ )
27656
+ return True
27657
+
27658
+ questions = self._avts_questions.get("questions", [])
27659
+ self.logger.info("Sample questions -> %s", questions)
27660
+
27661
+ response = self._avts.set_questions(questions=questions)
27662
+
27663
+ if response is None:
27664
+ self.logger.error("Aviator Search setting questions failed")
27665
+ success = False
27666
+ else:
27667
+ self.logger.info("Aviator Search questions set succesfully")
27668
+ self.logger.debug("%s", response)
27669
+
27670
+ self.write_status_file(
27671
+ success=success,
27672
+ payload_section_name=section_name,
27673
+ payload_section=self._avts_questions,
27674
+ )
27675
+
27676
+ return success
27677
+
27678
+ # end method definition
27679
+
27680
+ def process_embeddings(self, section_name: str = "embeddings") -> bool:
27681
+ """Process additional Aviator embeddings.
27682
+
27683
+ Args:
27684
+ section_name (str, optional):
27685
+ The name of the payload section. It can be overridden
27686
+ for cases where multiple sections of same type
27687
+ are used (e.g. the "Post" sections).
27688
+ This name is also used for the "success" status
27689
+ files written to the Admin Personal Workspace.
27690
+
27691
+ Returns:
27692
+ bool:
27693
+ True, if payload has been processed without errors, False otherwise.
27694
+
27695
+ """
27696
+
27697
+ if not self._embeddings:
26462
27698
  self.logger.info(
26463
27699
  "Payload section -> '%s' is empty. Skipping...",
26464
27700
  section_name,
@@ -26472,40 +27708,95 @@ class Payload:
26472
27708
 
26473
27709
  success: bool = True
26474
27710
 
26475
- for item in self._feme:
27711
+ for embedding in self._embeddings:
26476
27712
  # Check if item has been explicitly disabled in payload
26477
27713
  # (enabled = false). In this case we skip the element:
26478
- if not item.get("enabled", True):
27714
+ if not embedding.get("enabled", True):
26479
27715
  self.logger.info(
26480
- "Payload for item -> '%s' is disabled for FEME. Skipping...",
26481
- item,
27716
+ "Payload for embedding -> '%s' is disabled for FEME. Skipping...",
27717
+ str(embedding),
26482
27718
  )
26483
27719
  continue
26484
27720
 
26485
- if item.get("id", None) is None:
27721
+ # Backwards-compatibility for old syntax "documents":
27722
+ if "document_metadata" not in embedding:
27723
+ document_metadata = bool(embedding.get("documents", False))
27724
+ else:
27725
+ document_metadata = bool(embedding.get("document_metadata", False))
27726
+
27727
+ # Backwards-compatibility for old syntax "workspaces":
27728
+ if "workspace_metadata" not in embedding:
27729
+ workspace_metadata = bool(embedding.get("workspaces", False))
27730
+ else:
27731
+ workspace_metadata = bool(embedding.get("workspace_metadata", False))
27732
+
27733
+ wait_for_completion = bool(embedding.get("wait_for_completion", True))
27734
+ crawl = bool(embedding.get("crawl", False))
27735
+ images = bool(embedding.get("images", False))
27736
+
27737
+ # We support 3 ways to determine the node ID(s):
27738
+ # 1. Node ID is specified directly in the payload using 'id = "..."'
27739
+ # 2. Node is is specified via a nickname of the node using 'nickname = "..."'
27740
+ # 3. Nodes are specified via the name of a workspace type. In this
27741
+ # case all nodes of workspace instances are considered.
27742
+
27743
+ node_id = None
27744
+
27745
+ if embedding.get("id", None) is None and "nickname" in embedding:
26486
27746
  response = self._otcs.get_node_from_nickname(
26487
- nickname=item.get("nickname"),
27747
+ nickname=embedding.get("nickname"),
26488
27748
  )
26489
27749
  node_id = self._otcs.get_result_value(response, "id")
26490
27750
 
26491
27751
  else:
26492
- node_id = item.get("id")
27752
+ node_id = embedding.get("id", None)
26493
27753
  response = None
26494
27754
 
26495
- self._otcs.feme_embedd_metadata(
26496
- node_id=node_id,
26497
- node=response,
26498
- wait_for_completion=bool(item.get("wait_for_completion")),
26499
- crawl=bool(item.get("crawl")),
26500
- document_metadata=bool(item.get("documents")),
26501
- workspace_metadata=bool(item.get("workspaces")),
26502
- images=bool(item.get("images")),
26503
- )
27755
+ if node_id:
27756
+ result = self._otcs.aviator_embed_metadata(
27757
+ node_id=node_id,
27758
+ node=response,
27759
+ wait_for_completion=wait_for_completion,
27760
+ crawl=crawl,
27761
+ document_metadata=document_metadata,
27762
+ workspace_metadata=workspace_metadata,
27763
+ images=images,
27764
+ )
27765
+ if not result:
27766
+ success = False
27767
+ elif "workspace_types" in embedding:
27768
+ workspace_types = embedding["workspace_types"]
27769
+ # Handle the case of a single workspace type name:
27770
+ if isinstance(workspace_types, str):
27771
+ workspace_types = [workspace_types]
27772
+ for workspace_type_name in workspace_types:
27773
+ self.logger.info(
27774
+ "Embedding metadata of workspace instances with type -> '%s'...", workspace_type_name
27775
+ )
27776
+ workspace_instances = self._otcs.get_workspace_instances_iterator(type_name=workspace_type_name)
27777
+ for workspace in workspace_instances:
27778
+ properties = workspace.get("data").get("properties")
27779
+ self.logger.info(
27780
+ "Embedding metadata of workspace instance -> '%s' (%s)",
27781
+ properties["name"],
27782
+ properties["id"],
27783
+ )
27784
+ result = self._otcs.aviator_embed_metadata(
27785
+ node_id=None,
27786
+ node=workspace,
27787
+ wait_for_completion=wait_for_completion,
27788
+ crawl=crawl,
27789
+ document_metadata=document_metadata,
27790
+ workspace_metadata=workspace_metadata,
27791
+ images=images,
27792
+ )
27793
+ if not result:
27794
+ success = False
26504
27795
 
26505
27796
  self.write_status_file(
26506
27797
  success=success,
26507
27798
  payload_section_name=section_name,
26508
- payload_section=self._feme,
27799
+ payload_section=self._embeddings,
26509
27800
  )
26510
27801
 
26511
27802
  return success
@@ -28108,239 +29399,6 @@ class Payload:
28108
29399
 
28109
29400
  # end method definition
28110
29401
 
28111
- def appworks_resource_payload(
28112
- self,
28113
- org_name: str,
28114
- username: str,
28115
- password: str,
28116
- ) -> dict:
28117
- """Create a Python dict with the special payload we need for AppWorks.
28118
-
28119
- Args:
28120
- org_name (str):
28121
- The name of the organization.
28122
- username (str):
28123
- The user name.
28124
- password (str):
28125
- The password.
28126
-
28127
- Returns:
28128
- dict:
28129
- AppWorks specific payload.
28130
-
28131
- """
28132
-
28133
- additional_payload = {}
28134
- additional_payload["connectorid"] = "rest"
28135
- additional_payload["resourceType"] = "rest"
28136
- user_attribute_mapping = [
28137
- {
28138
- "sourceAttr": ["oTExternalID1"],
28139
- "destAttr": "__NAME__",
28140
- "mappingFormat": "%s",
28141
- },
28142
- {
28143
- "sourceAttr": ["displayname"],
28144
- "destAttr": "DisplayName",
28145
- "mappingFormat": "%s",
28146
- },
28147
- {"sourceAttr": ["mail"], "destAttr": "Email", "mappingFormat": "%s"},
28148
- {
28149
- "sourceAttr": ["oTTelephoneNumber"],
28150
- "destAttr": "Telephone",
28151
- "mappingFormat": "%s",
28152
- },
28153
- {
28154
- "sourceAttr": ["oTMobile"],
28155
- "destAttr": "Mobile",
28156
- "mappingFormat": "%s",
28157
- },
28158
- {
28159
- "sourceAttr": ["oTFacsimileTelephoneNumber"],
28160
- "destAttr": "Fax",
28161
- "mappingFormat": "%s",
28162
- },
28163
- {
28164
- "sourceAttr": ["oTStreetAddress,l,st,postalCode,c"],
28165
- "destAttr": "Address",
28166
- "mappingFormat": "%s%n%s %s %s%n%s",
28167
- },
28168
- {
28169
- "sourceAttr": ["oTCompany"],
28170
- "destAttr": "Company",
28171
- "mappingFormat": "%s",
28172
- },
28173
- {
28174
- "sourceAttr": ["ds-pwp-account-disabled"],
28175
- "destAttr": "AccountDisabled",
28176
- "mappingFormat": "%s",
28177
- },
28178
- {
28179
- "sourceAttr": ["oTExtraAttr9"],
28180
- "destAttr": "IsServiceAccount",
28181
- "mappingFormat": "%s",
28182
- },
28183
- {
28184
- "sourceAttr": ["custom:proxyConfiguration"],
28185
- "destAttr": "ProxyConfiguration",
28186
- "mappingFormat": "%s",
28187
- },
28188
- {
28189
- "sourceAttr": ["c"],
28190
- "destAttr": "Identity-CountryOrRegion",
28191
- "mappingFormat": "%s",
28192
- },
28193
- {
28194
- "sourceAttr": ["gender"],
28195
- "destAttr": "Identity-Gender",
28196
- "mappingFormat": "%s",
28197
- },
28198
- {
28199
- "sourceAttr": ["displayName"],
28200
- "destAttr": "Identity-DisplayName",
28201
- "mappingFormat": "%s",
28202
- },
28203
- {
28204
- "sourceAttr": ["oTStreetAddress"],
28205
- "destAttr": "Identity-Address",
28206
- "mappingFormat": "%s",
28207
- },
28208
- {
28209
- "sourceAttr": ["l"],
28210
- "destAttr": "Identity-City",
28211
- "mappingFormat": "%s",
28212
- },
28213
- {
28214
- "sourceAttr": ["mail"],
28215
- "destAttr": "Identity-Email",
28216
- "mappingFormat": "%s",
28217
- },
28218
- {
28219
- "sourceAttr": ["givenName"],
28220
- "destAttr": "Identity-FirstName",
28221
- "mappingFormat": "%s",
28222
- },
28223
- {
28224
- "sourceAttr": ["sn"],
28225
- "destAttr": "Identity-LastName",
28226
- "mappingFormat": "%s",
28227
- },
28228
- {
28229
- "sourceAttr": ["initials"],
28230
- "destAttr": "Identity-MiddleNames",
28231
- "mappingFormat": "%s",
28232
- },
28233
- {
28234
- "sourceAttr": ["oTMobile"],
28235
- "destAttr": "Identity-Mobile",
28236
- "mappingFormat": "%s",
28237
- },
28238
- {
28239
- "sourceAttr": ["postalCode"],
28240
- "destAttr": "Identity-PostalCode",
28241
- "mappingFormat": "%s",
28242
- },
28243
- {
28244
- "sourceAttr": ["st"],
28245
- "destAttr": "Identity-StateOrProvince",
28246
- "mappingFormat": "%s",
28247
- },
28248
- {
28249
- "sourceAttr": ["title"],
28250
- "destAttr": "Identity-title",
28251
- "mappingFormat": "%s",
28252
- },
28253
- {
28254
- "sourceAttr": ["physicalDeliveryOfficeName"],
28255
- "destAttr": "Identity-physicalDeliveryOfficeName",
28256
- "mappingFormat": "%s",
28257
- },
28258
- {
28259
- "sourceAttr": ["oTFacsimileTelephoneNumber"],
28260
- "destAttr": "Identity-oTFacsimileTelephoneNumber",
28261
- "mappingFormat": "%s",
28262
- },
28263
- {
28264
- "sourceAttr": ["notes"],
28265
- "destAttr": "Identity-notes",
28266
- "mappingFormat": "%s",
28267
- },
28268
- {
28269
- "sourceAttr": ["oTCompany"],
28270
- "destAttr": "Identity-oTCompany",
28271
- "mappingFormat": "%s",
28272
- },
28273
- {
28274
- "sourceAttr": ["oTDepartment"],
28275
- "destAttr": "Identity-oTDepartment",
28276
- "mappingFormat": "%s",
28277
- },
28278
- {
28279
- "sourceAttr": ["birthDate"],
28280
- "destAttr": "Identity-Birthday",
28281
- "mappingFormat": "%s",
28282
- },
28283
- {
28284
- "sourceAttr": ["cn"],
28285
- "destAttr": "Identity-UserName",
28286
- "mappingFormat": "%s",
28287
- },
28288
- {
28289
- "sourceAttr": ["Description"],
28290
- "destAttr": "Identity-UserDescription",
28291
- "mappingFormat": "%s",
28292
- },
28293
- {
28294
- "sourceAttr": ["oTTelephoneNumber"],
28295
- "destAttr": "Identity-Phone",
28296
- "mappingFormat": "%s",
28297
- },
28298
- {
28299
- "sourceAttr": ["displayName"],
28300
- "destAttr": "Identity-IdentityDisplayName",
28301
- "mappingFormat": "%s",
28302
- },
28303
- ]
28304
- additional_payload["userAttributeMapping"] = user_attribute_mapping
28305
- group_attribute_mapping = [
28306
- {
28307
- "sourceAttr": ["cn"],
28308
- "destAttr": "__NAME__",
28309
- "mappingFormat": '%js:function format(name) { return name.replace(/&/g,"-and-"); }',
28310
- },
28311
- {
28312
- "sourceAttr": ["description"],
28313
- "destAttr": "Description",
28314
- "mappingFormat": "%s",
28315
- },
28316
- {
28317
- "sourceAttr": ["description"],
28318
- "destAttr": "Identity-Description",
28319
- "mappingFormat": "%s",
28320
- },
28321
- {
28322
- "sourceAttr": ["displayName"],
28323
- "destAttr": "Identity-DisplayName",
28324
- "mappingFormat": "%s",
28325
- },
28326
- ]
28327
- additional_payload["groupAttributeMapping"] = group_attribute_mapping
28328
- additional_payload["connectorName"] = "REST (Generic)"
28329
- additional_payload["pcCreatePermissionAllowed"] = "true"
28330
- additional_payload["pcModifyPermissionAllowed"] = "true"
28331
- additional_payload["pcDeletePermissionAllowed"] = "false"
28332
- additional_payload["connectionParamInfo"] = [
28333
- {
28334
- "name": "fBaseURL",
28335
- "value": "http://appworks:8080/home/" + org_name + "/app/otdspush",
28336
- },
28337
- {"name": "fUsername", "value": username},
28338
- {"name": "fPassword", "value": password},
28339
- ]
28340
- return additional_payload
28341
-
28342
- # end method definition
28343
-
28344
29402
  def start_impersonation(self, username: str, otcs_object: OTCS | None = None) -> bool:
28345
29403
  """Impersonate to a defined user.
28346
29404