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.
- pyxecm/__init__.py +2 -1
- pyxecm/avts.py +79 -33
- pyxecm/customizer/api/app.py +45 -796
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +25 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +568 -326
- pyxecm/customizer/customizer.py +204 -430
- pyxecm/customizer/guidewire.py +907 -43
- pyxecm/customizer/k8s.py +243 -56
- pyxecm/customizer/m365.py +104 -15
- pyxecm/customizer/payload.py +1943 -885
- pyxecm/customizer/pht.py +19 -2
- pyxecm/customizer/servicenow.py +22 -5
- pyxecm/customizer/settings.py +9 -6
- pyxecm/helper/xml.py +69 -0
- pyxecm/otac.py +1 -1
- pyxecm/otawp.py +2104 -1535
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +201 -37
- pyxecm/otds.py +35 -13
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-2.0.0.dist-info/RECORD +0 -54
- /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/customizer/payload.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
434
|
-
list
|
|
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
|
-
-
|
|
713
|
-
|
|
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
|
-
|
|
1204
|
+
_appworks_configurations = []
|
|
1153
1205
|
|
|
1154
1206
|
_avts_repositories: list = []
|
|
1155
1207
|
|
|
1156
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1530
|
-
"""
|
|
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 `
|
|
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:
|
|
1606
|
+
bool:
|
|
1607
|
+
True, if payload has been processed without errors, False otherwise.
|
|
1538
1608
|
|
|
1539
1609
|
"""
|
|
1540
1610
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
1623
|
+
success: bool = True
|
|
1569
1624
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1601
|
-
self._otds.
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
-
|
|
1613
|
-
|
|
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
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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.
|
|
1624
|
-
"
|
|
1625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
-
"
|
|
1705
|
-
|
|
1834
|
+
"Partition -> '%s' is already licensed for -> '%s' (%s)",
|
|
1835
|
+
partition_name,
|
|
1706
1836
|
product_name,
|
|
1707
|
-
|
|
1837
|
+
"USERS",
|
|
1708
1838
|
)
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1855
|
+
self.logger.info(
|
|
1856
|
+
"Partition -> '%s' successfully assigned to license -> '%s' (%s)",
|
|
1726
1857
|
partition_name,
|
|
1727
|
-
|
|
1858
|
+
product_name,
|
|
1728
1859
|
"USERS",
|
|
1729
|
-
license_name,
|
|
1730
1860
|
)
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1904
|
+
self._log_header_callback(
|
|
1905
|
+
text="Process AppWorks workspaces, project, entities for '{}'".format(organization), char="-"
|
|
1906
|
+
)
|
|
1755
1907
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
)
|
|
1799
|
-
|
|
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=
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
1815
|
-
self._otawp.
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
|
|
1837
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3214
|
+
text="Process additional Embeddings for Content Aviator",
|
|
2775
3215
|
)
|
|
2776
|
-
self.
|
|
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 "
|
|
2783
|
-
self._log_header_callback(
|
|
2784
|
-
|
|
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
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
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
|
|
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.
|
|
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.
|
|
3962
|
+
"Synchronized partition -> '%s' does already exist.",
|
|
3530
3963
|
partition_name,
|
|
3531
3964
|
)
|
|
3532
|
-
|
|
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
|
-
"
|
|
3968
|
+
"Synchronized partition -> '%s' does not yet exist. Creating...",
|
|
3548
3969
|
partition_name,
|
|
3549
3970
|
)
|
|
3550
|
-
|
|
3551
|
-
self.
|
|
3552
|
-
|
|
3553
|
-
|
|
3971
|
+
|
|
3972
|
+
response = self._otds.add_synchronized_partition(
|
|
3973
|
+
name=partition_name,
|
|
3974
|
+
description=partition_description,
|
|
3975
|
+
data=partition["spec"],
|
|
3554
3976
|
)
|
|
3555
|
-
|
|
3556
|
-
|
|
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
|
-
|
|
3612
|
-
for
|
|
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=
|
|
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.
|
|
4177
|
+
"Partition -> '%s' does already exist.",
|
|
3705
4178
|
partition_name,
|
|
3706
4179
|
)
|
|
3707
|
-
|
|
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
|
-
"
|
|
4183
|
+
"Partition -> '%s' does not yet exist. Creating...",
|
|
3722
4184
|
partition_name,
|
|
3723
|
-
" ({})".format(partition_description) if partition_description else "",
|
|
3724
4185
|
)
|
|
3725
|
-
|
|
3726
|
-
self.
|
|
3727
|
-
|
|
3728
|
-
|
|
4186
|
+
|
|
4187
|
+
response = self._otds.add_partition(
|
|
4188
|
+
name=partition_name,
|
|
4189
|
+
description=partition_description,
|
|
3729
4190
|
)
|
|
3730
|
-
|
|
3731
|
-
|
|
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
|
-
|
|
3771
|
-
for
|
|
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=
|
|
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
|
|
3808
|
-
|
|
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
|
|
4335
|
+
True if payload has been processed without errors, False otherwise
|
|
3826
4336
|
|
|
3827
4337
|
"""
|
|
3828
4338
|
|
|
3829
|
-
if not self.
|
|
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
|
|
3844
|
-
|
|
3845
|
-
if not
|
|
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
|
|
3855
|
-
|
|
4357
|
+
"Payload for License -> '%s' is disabled. Skipping...",
|
|
4358
|
+
lic,
|
|
3856
4359
|
)
|
|
3857
4360
|
continue
|
|
3858
4361
|
|
|
3859
|
-
|
|
3860
|
-
if not
|
|
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
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
self.
|
|
3877
|
-
|
|
3878
|
-
|
|
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
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
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
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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
|
-
|
|
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["
|
|
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.
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
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'
|
|
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
|
|
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
|
-
#
|
|
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
|
|
12510
|
-
|
|
12511
|
-
|
|
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
|
-
|
|
12515
|
-
continue
|
|
13053
|
+
# end if related_workspace is not None
|
|
12516
13054
|
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
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
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
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
|
-
**
|
|
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
|
-
"
|
|
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
|
|
15828
|
+
self.logger.error("Failed to update scheduled bot item -> '%s'.", item_name)
|
|
15210
15829
|
success = False
|
|
15211
15830
|
continue
|
|
15212
15831
|
|
|
15213
|
-
|
|
15214
|
-
if
|
|
15215
|
-
self.logger.
|
|
15216
|
-
|
|
15217
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
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
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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("
|
|
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("
|
|
18312
|
+
self.logger.info("%s automation -> '%s' is not having password.", automation_type, name)
|
|
17331
18313
|
|
|
17332
|
-
|
|
17333
|
-
if not
|
|
18314
|
+
automation_steps = browser_automation.get("automations", [])
|
|
18315
|
+
if not automation_steps:
|
|
17334
18316
|
self.logger.error(
|
|
17335
|
-
"
|
|
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("
|
|
17345
|
-
self.logger.info("
|
|
17346
|
-
|
|
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
|
-
#
|
|
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.
|
|
18352
|
+
browser_automation_object.set_timeout(wait_time=wait_time)
|
|
17362
18353
|
if "wait_time" in browser_automation:
|
|
17363
18354
|
self.logger.info(
|
|
17364
|
-
"
|
|
18355
|
+
"%s Automation wait time -> '%s' configured.",
|
|
18356
|
+
automation_type,
|
|
17365
18357
|
wait_time,
|
|
17366
18358
|
)
|
|
17367
18359
|
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
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
|
-
|
|
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
|
|
18393
|
+
match automation_step_type:
|
|
17378
18394
|
case "login":
|
|
17379
|
-
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
|
-
|
|
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.
|
|
18422
|
+
"Cannot log into -> %s. Skipping to next automation step...",
|
|
17399
18423
|
base_url + page,
|
|
17400
18424
|
)
|
|
17401
18425
|
success = False
|
|
17402
|
-
|
|
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 =
|
|
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
|
-
|
|
18436
|
+
"Automation step type -> '%s' requires 'page' parameter. Stopping automation.",
|
|
18437
|
+
automation_step_type,
|
|
17413
18438
|
)
|
|
17414
18439
|
success = False
|
|
17415
18440
|
break
|
|
17416
|
-
|
|
17417
|
-
|
|
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
|
|
18479
|
+
"Cannot load page -> %s. Skipping this step...",
|
|
17420
18480
|
page,
|
|
17421
18481
|
)
|
|
17422
18482
|
success = False
|
|
17423
|
-
|
|
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
|
-
|
|
17430
|
-
|
|
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
|
|
17433
|
-
|
|
18494
|
+
"Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
|
|
18495
|
+
automation_step_type,
|
|
17434
18496
|
)
|
|
17435
18497
|
success = False
|
|
17436
18498
|
break
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
|
|
17441
|
-
|
|
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
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
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
|
-
|
|
17449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17456
|
-
|
|
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
|
|
17459
|
-
|
|
18539
|
+
"Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
|
|
18540
|
+
automation_step_type,
|
|
17460
18541
|
)
|
|
17461
18542
|
success = False
|
|
17462
18543
|
break
|
|
17463
|
-
|
|
17464
|
-
|
|
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
|
|
17468
|
-
|
|
17469
|
-
|
|
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
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
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
|
-
"
|
|
17483
|
-
|
|
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
|
|
17490
|
-
|
|
17491
|
-
|
|
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
|
|
17496
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
27714
|
+
if not embedding.get("enabled", True):
|
|
26479
27715
|
self.logger.info(
|
|
26480
|
-
"Payload for
|
|
26481
|
-
|
|
27716
|
+
"Payload for embedding -> '%s' is disabled for FEME. Skipping...",
|
|
27717
|
+
str(embedding),
|
|
26482
27718
|
)
|
|
26483
27719
|
continue
|
|
26484
27720
|
|
|
26485
|
-
|
|
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=
|
|
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 =
|
|
27752
|
+
node_id = embedding.get("id", None)
|
|
26493
27753
|
response = None
|
|
26494
27754
|
|
|
26495
|
-
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
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.
|
|
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
|
|