pyxecm 2.0.1__py3-none-any.whl → 2.0.3__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.

@@ -67,9 +67,9 @@ from dateutil.parser import parse
67
67
  from lark import exceptions as lark_exceptions # used by hcl2
68
68
 
69
69
  # OpenText specific modules:
70
- from pyxecm import AVTS, OTAC, OTAWP, OTCS, OTDS, OTIV, OTMM, OTPD, CoreShare
70
+ from pyxecm import AVTS, OTAC, OTAWP, OTCA, OTCS, OTDS, OTIV, OTKD, OTMM, OTPD, CoreShare
71
71
  from pyxecm.customizer.browser_automation import BrowserAutomation
72
- from pyxecm.customizer.exceptions import StopOnError
72
+ from pyxecm.customizer.exceptions import PayloadImportError, StopOnError
73
73
  from pyxecm.customizer.k8s import K8s
74
74
  from pyxecm.customizer.m365 import M365
75
75
  from pyxecm.customizer.pht import PHT
@@ -160,23 +160,18 @@ def load_payload(
160
160
  except (
161
161
  lark_exceptions.UnexpectedToken,
162
162
  lark_exceptions.UnexpectedCharacters,
163
- ):
164
- logger.error(
165
- "Syntax error while reading Terraform payload file -> '%s'!",
166
- payload_source,
167
- )
168
- payload = {}
163
+ ) as exc:
164
+ exception = f"Syntax error while reading Terraform payload file -> '{payload_source}'! --> {traceback.format_exception_only(exc)}"
165
+ raise PayloadImportError(exception) from exc
166
+
169
167
  except (
170
168
  FileNotFoundError,
171
169
  ImportError,
172
170
  ValueError,
173
171
  SyntaxError,
174
- ):
175
- logger.error(
176
- "Error while reading Terraform payload file -> '%s'",
177
- payload_source,
178
- )
179
- payload = {}
172
+ ) as exc:
173
+ exception = f"Error while reading Terraform payload file -> '{payload_source}'! --> {traceback.format_exception_only(exc)}"
174
+ raise PayloadImportError(exception) from exc
180
175
 
181
176
  elif payload_source.endswith(".yml.gz.b64"):
182
177
  logger.info("Open payload from base64-gz-YAML file -> '%s'", payload_source)
@@ -239,9 +234,11 @@ class Payload:
239
234
  _successfactors: SuccessFactors | None
240
235
  _salesforce: Salesforce | None
241
236
  _servicenow: ServiceNow | None
242
- _browser_automation: BrowserAutomation | None
243
237
  _custom_settings_dir = ""
244
238
  _otawp: OTAWP | None
239
+ _otca: OTCA | None
240
+ _otkd: OTKD | None
241
+ _avts: AVTS | None
245
242
 
246
243
  # _payload_source (string): This is either path + filename of the yaml payload
247
244
  # or an path + filename of the Terraform HCL payload
@@ -411,6 +408,7 @@ class Payload:
411
408
  - title (str, optional, default = "")
412
409
  - email (str, optional, default = "")
413
410
  - base_group (str, optional, default = "DefaultGroup")
411
+ - user_type (str, optional, default = "User") - possible values are "User" and "ServiceUser"
414
412
  - company (str, optional, default = "Innovate") - currently used for Salesforce users only
415
413
  - privileges (list, optional, default = ["Login", "Public Access"])
416
414
  - groups (list, optional)
@@ -567,7 +565,12 @@ class Payload:
567
565
  * role (name)
568
566
  * users (list, optional, default = [])
569
567
  * groups (list, optional, default = [])
570
- - relationships (list, optional, default = []) - list of strings with logical workspace IDs
568
+ - relationships (list, optional, default = []) - list of related workspaces.
569
+ The elements of the list can be:
570
+ * string or integer with logical workspace ID
571
+ * string with nickname of the related workspace
572
+ * dictionaries with keys "type" and "name" of the related workspace
573
+ * list of strings with the top-down path in the Enterprise volume
571
574
  """
572
575
  _workspaces = []
573
576
 
@@ -636,6 +639,8 @@ class Payload:
636
639
  - name (str, mandatory)
637
640
  - nodeid (int, mandatory if no volume is specified) - this is the technical OTCS ID - typically only known for some preinstalled items
638
641
  - volume (int, mandatory if no nodeid is specified)
642
+ - path (list, optional) - can be combined with volume - to specify a top-down path in the volume to the item to be renamed
643
+ - nickname (str, optional) - the nickname of the node to rename - alternative to to volume/path or nodeid
639
644
  """
640
645
  _renamings = []
641
646
 
@@ -747,7 +752,7 @@ class Payload:
747
752
  - base_url (str, mandatory)
748
753
  - user_name (str, optional)
749
754
  - password (str, optional)
750
- - wait_time (float, optional, default = 15.0) - wait time in seconds
755
+ - wait_time (float, optional, default = 30.0) - wait time in seconds
751
756
  - wait_until (str, optional) - the page load / navigation `wait until` strategy. Possible values: `load`, `networkidle`, `domcontentloaded`
752
757
  - debug (bool, optional, default = False) - if True take screenshots and save to container
753
758
  - automations (list, mandatory)
@@ -1189,6 +1194,8 @@ class Payload:
1189
1194
 
1190
1195
  _bulk_classifications = []
1191
1196
 
1197
+ _nifi_flows = []
1198
+
1192
1199
  _placeholder_values = {}
1193
1200
 
1194
1201
  # Link to the method in customizer.py to restart the Content Server pods.
@@ -1224,7 +1231,6 @@ class Payload:
1224
1231
  otpd_object: OTPD | None,
1225
1232
  m365_object: M365 | None,
1226
1233
  core_share_object: CoreShare | None,
1227
- browser_automation_object: BrowserAutomation | None,
1228
1234
  placeholder_values: dict,
1229
1235
  log_header_callback: Callable,
1230
1236
  browser_headless: bool = True,
@@ -1232,6 +1238,8 @@ class Payload:
1232
1238
  aviator_enabled: bool = False,
1233
1239
  upload_status_files: bool = True,
1234
1240
  otawp_object: OTAWP | None = None,
1241
+ otca_object: OTCA | None = None,
1242
+ otkd_object: OTKD | None = None,
1235
1243
  avts_object: AVTS | None = None,
1236
1244
  logger: logging.Logger = default_logger,
1237
1245
  ) -> None:
@@ -1266,8 +1274,6 @@ class Payload:
1266
1274
  The M365 object to talk to Microsoft Graph API.
1267
1275
  core_share_object (CoreShare | None):
1268
1276
  The Core Share object.
1269
- browser_automation_object (BrowserAutomation):
1270
- The BrowserAutomation object to automate things which don't have a REST API.
1271
1277
  placeholder_values (dict):
1272
1278
  A dictionary of placeholder values to be replaced in admin settings.
1273
1279
  log_header_callback:
@@ -1284,6 +1290,10 @@ class Payload:
1284
1290
  of the admin user in Content Server.
1285
1291
  otawp_object (OTAWP):
1286
1292
  An optional AppWorks Platform object.
1293
+ otca_object (OTCA):
1294
+ An optional Content Aviator object.
1295
+ otkd_object (OTKD):
1296
+ An optional Knowledge Discovery object.
1287
1297
  avts_object (AVTS):
1288
1298
  An optional Aviator Search object.
1289
1299
  logger (logging.Logger, optional):
@@ -1316,8 +1326,9 @@ class Payload:
1316
1326
  self._otcs_source = None
1317
1327
  self._pht = None # the OpenText prodcut hierarchy
1318
1328
  self._nhc = None # National Hurricane Center
1319
- self._avts = avts_object
1320
- self._browser_automation = browser_automation_object
1329
+ self._otca = otca_object # Content Aviator
1330
+ self._otkd = otkd_object # Knowledge Discovery
1331
+ self._avts = avts_object # Aviator Search
1321
1332
  self._browser_headless = browser_headless
1322
1333
  self._custom_settings_dir = custom_settings_dir
1323
1334
  self._placeholder_values = placeholder_values
@@ -1504,6 +1515,7 @@ class Payload:
1504
1515
  self._avts_repositories = self.get_payload_section("avtsRepositories")
1505
1516
  self._avts_questions = self.get_payload_section("avtsQuestions")
1506
1517
  self._embeddings = self.get_payload_section("embeddings")
1518
+ self._nifi_flows = self.get_payload_section("nifi")
1507
1519
 
1508
1520
  return self._payload
1509
1521
 
@@ -2153,7 +2165,7 @@ class Payload:
2153
2165
  case "customer":
2154
2166
  customer = self._otawp.get_customer_by_name(name=entity.get("name"))
2155
2167
  if customer:
2156
- customer_id = self._otawp.get_entity_value(entity=case_type, key="id")
2168
+ customer_id = self._otawp.get_entity_value(entity=customer, key="id")
2157
2169
  self.logger.info(
2158
2170
  "Customer -> '%s' (%s) does already exist. Skipping...", entity.get("name"), str(customer_id)
2159
2171
  )
@@ -2763,7 +2775,7 @@ class Payload:
2763
2775
  return group["id"]
2764
2776
  else:
2765
2777
  self.logger.debug(
2766
- "Did not find an existing group with name -> '%s'",
2778
+ "Cannot find an existing group -> '%s'",
2767
2779
  group_name,
2768
2780
  )
2769
2781
  return 0
@@ -2814,7 +2826,7 @@ class Payload:
2814
2826
  return group["m365_id"]
2815
2827
  else:
2816
2828
  self.logger.debug(
2817
- "Did not find an existing M365 group with name -> '%s'",
2829
+ "Cannot find an existing M365 group -> '%s'",
2818
2830
  group_name,
2819
2831
  )
2820
2832
  return None
@@ -2868,7 +2880,7 @@ class Payload:
2868
2880
  return group["core_share_id"]
2869
2881
  else:
2870
2882
  self.logger.debug(
2871
- "Did not find an existing Core Share group with name -> '%s'",
2883
+ "Cannot find an existing Core Share group -> '%s'",
2872
2884
  group["name"],
2873
2885
  )
2874
2886
  return None
@@ -2903,7 +2915,10 @@ class Payload:
2903
2915
  self.logger.error("User needs a login name to lookup the ID!")
2904
2916
  return 0
2905
2917
 
2906
- response = self._otcs.get_user(name=user_name)
2918
+ user_type = 17 if user.get("type", "User") == "ServiceUser" else 0
2919
+
2920
+ response = self._otcs.get_user(name=user_name, user_type=user_type)
2921
+
2907
2922
  # We use the lookup method here as get_user() could deliver more
2908
2923
  # then 1 result element (in edge cases):
2909
2924
  user_id = self._otcs.lookup_result_value(
@@ -2920,7 +2935,7 @@ class Payload:
2920
2935
  return user["id"]
2921
2936
  else:
2922
2937
  self.logger.debug(
2923
- "Did not find an existing user with name -> '%s'!",
2938
+ "Cannot find an existing user -> '%s'!",
2924
2939
  user_name,
2925
2940
  )
2926
2941
  return 0
@@ -2969,7 +2984,7 @@ class Payload:
2969
2984
  return user["m365_id"]
2970
2985
  else:
2971
2986
  self.logger.debug(
2972
- "Did not find an existing M365 user with name -> '%s'",
2987
+ "Did not find an existing M365 user -> '%s'",
2973
2988
  user_name,
2974
2989
  )
2975
2990
  return None
@@ -3029,11 +3044,17 @@ class Payload:
3029
3044
  user["core_share_id"] = core_share_user_id
3030
3045
  return user["core_share_id"]
3031
3046
  else:
3032
- self.logger.debug(
3033
- "Did not find an existing Core Share user with name -> '%s %s'",
3034
- user["firstname"],
3035
- user["lastname"],
3036
- )
3047
+ if "email" in user:
3048
+ self.logger.debug(
3049
+ "Did not find an existing Core Share user with email -> '%s'",
3050
+ user["email"],
3051
+ )
3052
+ else:
3053
+ self.logger.debug(
3054
+ "Cannot find an existing Core Share user -> '%s %s'",
3055
+ user.get("firstname"),
3056
+ user.get("lastname"),
3057
+ )
3037
3058
  return None
3038
3059
 
3039
3060
  # end method definition
@@ -3650,6 +3671,9 @@ class Payload:
3650
3671
  if not self._business_object_types:
3651
3672
  self._log_header_callback(text="Process Business Object Types")
3652
3673
  self.process_business_object_types()
3674
+ case "nifi":
3675
+ self._log_header_callback("Process Knowledge Discovery Nifi Flows")
3676
+ self.process_nifi_flows()
3653
3677
  case _:
3654
3678
  self.logger.error(
3655
3679
  "Illegal payload section name -> '%s' in payloadSections!",
@@ -5397,7 +5421,7 @@ class Payload:
5397
5421
  self.logger.error("Group -> '%s' does not have an ID.", group["name"])
5398
5422
  success = False
5399
5423
  continue
5400
- parent_group_names = group["parent_groups"]
5424
+ parent_group_names = group.get("parent_groups", [])
5401
5425
  for parent_group_name in parent_group_names:
5402
5426
  # First, try to find parent group in payload by parent group name:
5403
5427
  parent_group = next(
@@ -5460,8 +5484,9 @@ class Payload:
5460
5484
  member_id=group["id"],
5461
5485
  group_id=parent_group_id,
5462
5486
  )
5487
+ # end for parent_group_name in parent_group_names:
5463
5488
 
5464
- # Assign application roles to the new user:
5489
+ # Assign application roles to the new group:
5465
5490
  application_roles = group.get("application_roles", [])
5466
5491
  for role in application_roles:
5467
5492
  group_partition = self._otcs.config()["partition"]
@@ -5999,7 +6024,7 @@ class Payload:
5999
6024
  )
6000
6025
  continue
6001
6026
  self.logger.info(
6002
- "Did not find an existing user with name '%s' - creating a new user...",
6027
+ "Cannot find an existing user -> '%s' - creating a new user...",
6003
6028
  user_name,
6004
6029
  )
6005
6030
 
@@ -6355,7 +6380,7 @@ class Payload:
6355
6380
  user_name,
6356
6381
  rfc_name,
6357
6382
  rfc_description,
6358
- rfc_params,
6383
+ str(rfc_params),
6359
6384
  )
6360
6385
 
6361
6386
  result = self._sap.call(
@@ -6837,7 +6862,7 @@ class Payload:
6837
6862
  )
6838
6863
  if not group:
6839
6864
  self.logger.error(
6840
- "Cannot find group with name -> '%s'. Cannot establish membership in Salesforce. Skipping to next group...",
6865
+ "Cannot find group -> '%s'. Cannot establish membership in Salesforce. Skipping to next group...",
6841
6866
  user_group,
6842
6867
  )
6843
6868
  success = False
@@ -7305,7 +7330,7 @@ class Payload:
7305
7330
  )
7306
7331
  if not group:
7307
7332
  self.logger.error(
7308
- "Cannot find group with name -> '%s'. Cannot establish membership in Core Share. Skipping to next group...",
7333
+ "Cannot find group -> '%s'. Cannot establish membership in Core Share. Skipping to next group...",
7309
7334
  user_group,
7310
7335
  )
7311
7336
  success = False
@@ -7880,8 +7905,8 @@ class Payload:
7880
7905
  """Process groups in payload and create matching Teams in Microsoft 365.
7881
7906
 
7882
7907
  We need to do this after the creation of the M365 users as we require
7883
- Group Owners to create teams. These are NOT the teams for Extended ECM
7884
- workspaces! Those are created by Scheduled Bots (Jobs) from Extended ECM!
7908
+ Group Owners to create teams. These are NOT the teams for OTCS
7909
+ workspaces! Those are created by Scheduled Bots (Jobs) from OTCS!
7885
7910
 
7886
7911
  Args:
7887
7912
  section_name (str, optional):
@@ -8285,7 +8310,7 @@ class Payload:
8285
8310
 
8286
8311
  workspace_name = workspace["name"]
8287
8312
  self.logger.info(
8288
- "Check if stale Microsoft 365 Teams with name -> '%s' exist...",
8313
+ "Check if stale Microsoft 365 Teams -> '%s' exist...",
8289
8314
  workspace_name,
8290
8315
  )
8291
8316
  self._m365.delete_teams(name=workspace_name)
@@ -8381,8 +8406,8 @@ class Payload:
8381
8406
  def process_sites_m365(self, section_name: str = "sitesM365") -> bool:
8382
8407
  """Process M365 groups in payload and configure SharePoint sites in Microsoft 365.
8383
8408
 
8384
- These are NOT the SharePoint sites for Extended ECM workspaces which are created
8385
- by Scheduled Bots (Jobs) from Extended ECM via the creation of MS teams
8409
+ These are NOT the SharePoint sites for Business Workspaces which are created
8410
+ by Scheduled Bots (Jobs) from OTCS via the creation of MS teams
8386
8411
  (each MS Team has a SharePoint site behind it)!
8387
8412
 
8388
8413
  The are the SharePoint sites for the departmental groups such as "Sales",
@@ -8882,6 +8907,19 @@ class Payload:
8882
8907
  "A restart of the Content Server service is required.",
8883
8908
  )
8884
8909
  restart_required = True
8910
+
8911
+ if admin_setting.get("restart", False):
8912
+ self.logger.info(
8913
+ "Immediate restart requested - restart of OTCS services...",
8914
+ )
8915
+ # Restart OTCS frontend and backend pods:
8916
+ self._otcs_restart_callback(
8917
+ backend=self._otcs_backend,
8918
+ frontend=self._otcs_frontend,
8919
+ )
8920
+
8921
+ restart_required = False
8922
+
8885
8923
  else:
8886
8924
  self.logger.error(
8887
8925
  "Admin settings file -> '%s' not found.",
@@ -9026,6 +9064,11 @@ class Payload:
9026
9064
  continue
9027
9065
  system_type = external_system["external_system_type"]
9028
9066
 
9067
+ self._log_header_callback(
9068
+ text="Process External System -> '{}' ({})".format(system_name, system_type),
9069
+ char="-",
9070
+ )
9071
+
9029
9072
  # Check if external system has been explicitly disabled in payload
9030
9073
  # (enabled = false). In this case we skip the element:
9031
9074
  if not external_system.get("enabled", True):
@@ -9538,6 +9581,11 @@ class Payload:
9538
9581
  )
9539
9582
  continue
9540
9583
 
9584
+ # We skip also user of type "ServiceUser":
9585
+ if user.get("type", "User") == "ServiceUser":
9586
+ self.logger.info("Skipping service user -> '%s'...", user_name)
9587
+ continue
9588
+
9541
9589
  user_id = user.get("id")
9542
9590
  if not user_id:
9543
9591
  self.logger.error(
@@ -9978,23 +10026,23 @@ class Payload:
9978
10026
  # we assume the nickname of the photo item equals the login name of the user
9979
10027
  # we also assume that the photos have been uploaded / transported into the target system
9980
10028
  for user in self._users:
9981
- if "lastname" not in user or "firstname" not in user:
10029
+ if "name" not in user:
9982
10030
  self.logger.error(
9983
- "User is missing last name or first name. Skipping to next user...",
10031
+ "User is missing login name. Skipping to next user...",
9984
10032
  )
9985
10033
  success = False
9986
10034
  continue
9987
10035
  user_login = user["name"]
9988
- user_last_name = user["lastname"]
9989
- user_first_name = user["firstname"]
9990
- user_name = user_first_name + " " + user_last_name
10036
+ user_last_name = user.get("lastname", "")
10037
+ user_first_name = user.get("firstname", "")
10038
+ user_name = "{} {}".format(user_first_name, user_last_name).strip()
9991
10039
 
9992
10040
  # Check if user has been explicitly disabled in payload
9993
10041
  # (enabled = false). In this case we skip the element:
9994
10042
  if not user.get("enabled", True):
9995
10043
  self.logger.info(
9996
10044
  "Payload for user -> '%s' is disabled. Skipping...",
9997
- user_name,
10045
+ user_login,
9998
10046
  )
9999
10047
  continue
10000
10048
 
@@ -10005,7 +10053,7 @@ class Payload:
10005
10053
  if not user.get("enable_core_share", False):
10006
10054
  self.logger.info(
10007
10055
  "User -> '%s' is not enabled for Core Share. Skipping...",
10008
- user_name,
10056
+ user_login,
10009
10057
  )
10010
10058
  continue
10011
10059
 
@@ -10520,7 +10568,7 @@ class Payload:
10520
10568
 
10521
10569
  if not self._business_object_types:
10522
10570
  self.logger.warning(
10523
- "List of business object types is empty / not initialized! Cannot lookup type with name -> '%s'",
10571
+ "List of business object types is empty / not initialized! Cannot lookup type -> '%s'",
10524
10572
  bo_type_name,
10525
10573
  )
10526
10574
  return None
@@ -10532,7 +10580,7 @@ class Payload:
10532
10580
  )
10533
10581
  if not business_object_type:
10534
10582
  self.logger.warning(
10535
- "Cannot find business object type with name -> '%s'",
10583
+ "Cannot find business object type -> '%s'",
10536
10584
  bo_type_name,
10537
10585
  )
10538
10586
  return None
@@ -10851,7 +10899,7 @@ class Payload:
10851
10899
  if role_id is None:
10852
10900
  # if member_role is None:
10853
10901
  self.logger.error(
10854
- "Workspace template -> '%s' does not have a role with name -> '%s'",
10902
+ "Workspace template -> '%s' does not have a role -> '%s'",
10855
10903
  template_name,
10856
10904
  member_role_name,
10857
10905
  )
@@ -12998,7 +13046,7 @@ class Payload:
12998
13046
  workspace["type_name"],
12999
13047
  )
13000
13048
 
13001
- # now determine the actual node IDs of the workspaces (has been created before):
13049
+ # now determine the actual node ID of the workspace (which should have been created before):
13002
13050
  workspace_node_id = int(self.determine_workspace_id(workspace=workspace))
13003
13051
  if not workspace_node_id:
13004
13052
  self.logger.warning(
@@ -13016,64 +13064,97 @@ class Payload:
13016
13064
 
13017
13065
  success: bool = True
13018
13066
 
13019
- for related_workspace_id in workspace["relationships"]:
13067
+ for related_workspace in workspace["relationships"]:
13020
13068
  # Initialize variable to determine if we found a related workspace:
13021
13069
  related_workspace_node_id = None
13070
+ found_by = ""
13022
13071
 
13023
- #
13024
- # 1. Option: Find the related workspace with the logical ID given in the payload:
13025
- #
13026
- related_workspace = next(
13027
- (item for item in self._workspaces if item["id"] == related_workspace_id),
13028
- None,
13029
- )
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"],
13072
+ if isinstance(related_workspace, (str, int)):
13073
+ #
13074
+ # 1. Option: Find the related workspace with the logical ID given in the payload:
13075
+ #
13076
+ related_workspace_payload = next(
13077
+ (item for item in self._workspaces if str(item["id"]) == str(related_workspace)),
13078
+ None,
13079
+ )
13080
+ if related_workspace_payload:
13081
+ if not related_workspace_payload.get("enabled", True):
13082
+ self.logger.info(
13083
+ "Payload for Related Workspace -> '%s' is disabled. Skipping...",
13084
+ related_workspace_payload["name"],
13085
+ )
13086
+ continue
13087
+
13088
+ related_workspace_node_id = self.determine_workspace_id(
13089
+ workspace=related_workspace_payload,
13035
13090
  )
13036
- continue
13091
+ if not related_workspace_node_id:
13092
+ self.logger.warning(
13093
+ "Related Workspace -> '%s' (type -> '%s') has no node ID (workspaces creation may have failed or name is different from payload). Skipping to next workspace...",
13094
+ related_workspace_payload["name"],
13095
+ related_workspace_payload["type_name"],
13096
+ )
13097
+ continue
13098
+ found_by = "logical ID -> '{}' in payload".format(related_workspace)
13099
+ # end if related_workspace_payload:
13037
13100
 
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"],
13101
+ #
13102
+ # 2. Option: Find the related workspace with nickname:
13103
+ #
13104
+ else:
13105
+ # See if a nickname exists the the provided related_workspace:
13106
+ response = self._otcs.get_node_from_nickname(nickname=related_workspace)
13107
+ related_workspace_node_id = self._otcs.get_result_value(
13108
+ response=response,
13109
+ key="id",
13046
13110
  )
13047
- continue
13048
- self.logger.debug(
13049
- "Related Workspace with logical ID -> %s has node ID -> %s",
13050
- related_workspace_id,
13051
- related_workspace_node_id,
13052
- )
13053
- # end if related_workspace is not None
13111
+ if related_workspace_node_id:
13112
+ found_by = "nickname -> '{}'".format(related_workspace)
13113
+ # end if isinstance(related_workspace_id, (str, int)):
13054
13114
 
13055
13115
  #
13056
- # 2. Option: Find the related workspace with nickname:
13116
+ # 3. Option: Find the related workspace type and name:
13057
13117
  #
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)
13118
+ elif isinstance(related_workspace, dict):
13119
+ related_workspace_type = related_workspace.get("type", None)
13120
+ related_workspace_name = related_workspace.get("name", None)
13121
+ if related_workspace_type and related_workspace_name:
13122
+ response = self._otcs.get_workspace_by_type_and_name(
13123
+ type_name=related_workspace_type, name=related_workspace_name
13124
+ )
13125
+ related_workspace_node_id = self._otcs.get_result_value(
13126
+ response=response,
13127
+ key="id",
13128
+ )
13129
+ if related_workspace_node_id:
13130
+ found_by = "type -> '{}' and name -> '{}'".format(
13131
+ related_workspace_type, related_workspace_name
13132
+ )
13133
+ #
13134
+ # 4. Option: Find the related workspace volume and path:
13135
+ #
13136
+ elif isinstance(related_workspace, list):
13137
+ response = self._otcs.get_node_by_volume_and_path(
13138
+ volume_type=self._otcs.VOLUME_TYPE_ENTERPRISE_WORKSPACE, path=related_workspace
13139
+ )
13061
13140
  related_workspace_node_id = self._otcs.get_result_value(
13062
13141
  response=response,
13063
13142
  key="id",
13064
13143
  )
13144
+ if related_workspace_node_id:
13145
+ found_by = "path -> {}".format(related_workspace)
13065
13146
 
13066
13147
  if related_workspace_node_id is None:
13067
13148
  self.logger.error(
13068
- "Related Workspace with logical ID or nickname -> %s not found.",
13069
- related_workspace_id,
13149
+ "Related Workspace -> %s not found.",
13150
+ related_workspace,
13070
13151
  )
13071
13152
  success = False
13072
13153
  continue
13073
13154
 
13074
13155
  self.logger.debug(
13075
- "Related Workspace with logical ID or nickname -> %s has node ID -> %s",
13076
- related_workspace_id,
13156
+ "Related Workspace with %s has node ID -> %s",
13157
+ found_by,
13077
13158
  related_workspace_node_id,
13078
13159
  )
13079
13160
 
@@ -13443,7 +13524,7 @@ class Payload:
13443
13524
  if role_id is None:
13444
13525
  # if member_role is None:
13445
13526
  self.logger.error(
13446
- "Workspace -> '%s' does not have a role with name -> '%s'",
13527
+ "Workspace -> '%s' does not have a role -> '%s'",
13447
13528
  workspace_name,
13448
13529
  member_role_name,
13449
13530
  )
@@ -13719,7 +13800,7 @@ class Payload:
13719
13800
  )
13720
13801
  if role_id is None:
13721
13802
  self.logger.error(
13722
- "Workspace -> '%s' does not have a role with name -> '%s'",
13803
+ "Workspace -> '%s' does not have a role -> '%s'",
13723
13804
  workspace_name,
13724
13805
  member_role_name,
13725
13806
  )
@@ -14197,7 +14278,12 @@ class Payload:
14197
14278
  )
14198
14279
  continue
14199
14280
 
14200
- user_partition = self._otcs.config()["partition"]
14281
+ self._log_header_callback(
14282
+ text="Process settings for user -> '{}'".format(user_name),
14283
+ char="-",
14284
+ )
14285
+
14286
+ user_partition = self._otcs.config().get("partition", None)
14201
14287
  if not user_partition:
14202
14288
  self.logger.error("User partition not found!")
14203
14289
  success = False
@@ -14312,9 +14398,6 @@ class Payload:
14312
14398
  # The following code (for loop) will change the authenticated user - we need to
14313
14399
  # switch it back to admin user later so we safe the admin credentials for this:
14314
14400
 
14315
- # save admin credentials for later switch back to admin user:
14316
- # admin_credentials = self._otcs.credentials() if self._users else {}
14317
-
14318
14401
  for user in self._users:
14319
14402
  user_name = user.get("name")
14320
14403
  if not user_name:
@@ -14323,6 +14406,11 @@ class Payload:
14323
14406
  )
14324
14407
  continue
14325
14408
 
14409
+ self._log_header_callback(
14410
+ text="Process Favorites and Profile for user -> '{}'".format(user_name),
14411
+ char="-",
14412
+ )
14413
+
14326
14414
  # Check if user has been explicitly disabled in payload
14327
14415
  # (enabled = false). In this case we skip the element:
14328
14416
  if not user.get("enabled", True):
@@ -14332,6 +14420,11 @@ class Payload:
14332
14420
  )
14333
14421
  continue
14334
14422
 
14423
+ # We skip also user of type "ServiceUser":
14424
+ if user.get("type", "User") == "ServiceUser":
14425
+ self.logger.info("Skipping service user -> '%s'...", user_name)
14426
+ continue
14427
+
14335
14428
  # Impersonate as the user:
14336
14429
  self.logger.info("Impersonate user -> '%s'...", user_name)
14337
14430
  result = self.start_impersonation(username=user_name)
@@ -15499,17 +15592,37 @@ class Payload:
15499
15592
  if "name" not in renaming:
15500
15593
  self.logger.error("Renamings require the new name!")
15501
15594
  continue
15502
- if "nodeid" not in renaming:
15503
- if "volume" not in renaming:
15504
- self.logger.error(
15505
- "Renamings require either a node ID or a volume! Skipping to next renaming...",
15595
+ if "nodeid" in renaming:
15596
+ node_id = renaming["nodeid"]
15597
+ elif "volume" in renaming:
15598
+ path = renaming.get("path")
15599
+ volume = renaming.get("volume")
15600
+ if path:
15601
+ self.logger.info(
15602
+ "Found path -> '%s' in renaming payload. Determine node ID by volume and path...",
15603
+ path,
15506
15604
  )
15507
- continue
15508
- # Determine object ID of volume:
15509
- volume = self._otcs.get_volume(volume_type=renaming["volume"])
15510
- node_id = self._otcs.get_result_value(response=volume, key="id")
15605
+ node = self._otcs.get_node_by_volume_and_path(
15606
+ volume_type=volume,
15607
+ path=path,
15608
+ )
15609
+ else:
15610
+ # Determine object ID of volume:
15611
+ node = self._otcs.get_volume(volume_type=volume)
15612
+ node_id = self._otcs.get_result_value(response=node, key="id")
15613
+ elif "nickname" in renaming:
15614
+ nickname = renaming["nickname"]
15615
+ self.logger.info(
15616
+ "Found nickname -> '%s' in renaming payload. Determine node ID by nickname...",
15617
+ nickname,
15618
+ )
15619
+ node = self._otcs.get_node_from_nickname(nickname=nickname)
15620
+ node_id = self._otcs.get_result_value(response=node, key="id")
15511
15621
  else:
15512
- node_id = renaming["nodeid"]
15622
+ self.logger.error(
15623
+ "Renamings require either a node ID or a volume (with an optional path) or a nickname! Skipping to next renaming...",
15624
+ )
15625
+ continue
15513
15626
 
15514
15627
  # Check if renaming has been explicitly disabled in payload
15515
15628
  # (enabled = false). In this case we skip this payload element:
@@ -15625,7 +15738,7 @@ class Payload:
15625
15738
  )
15626
15739
  success = False
15627
15740
  continue
15628
- else:
15741
+ elif parent_path is not None: # parent_path can be [] which is valid for top-level items!
15629
15742
  parent_volume = item.get("parent_volume", self._otcs.VOLUME_TYPE_ENTERPRISE_WORKSPACE)
15630
15743
  parent_node = self._otcs.get_node_by_volume_and_path(
15631
15744
  volume_type=parent_volume,
@@ -15636,11 +15749,17 @@ class Payload:
15636
15749
  if not parent_id:
15637
15750
  # if not parent_node:
15638
15751
  self.logger.error(
15639
- "Item -> '%s' has a parent path that does not exist. Skipping...",
15752
+ "Item -> '%s' has a parent path -> %s that does not exist and couldn't be created in volume -> %d. Skipping...",
15640
15753
  item_name,
15754
+ parent_path,
15755
+ self._otcs.VOLUME_TYPE_ENTERPRISE_WORKSPACE,
15641
15756
  )
15642
15757
  success = False
15643
15758
  continue
15759
+ else:
15760
+ self.logger.error("The parent for the item -> '%s' is not specified by nickname nor path!", item_name)
15761
+ success = False
15762
+ continue
15644
15763
 
15645
15764
  # Handling for shortcut items that have an orginal node:
15646
15765
  original_nickname = item.get("original_nickname")
@@ -15663,9 +15782,10 @@ class Payload:
15663
15782
  )
15664
15783
  success = False
15665
15784
  continue
15666
- elif original_path:
15785
+ elif original_path is not None: # original_path can be [] which is valid for top-level items!
15786
+ original_volume = item.get("original_volume", self._otcs.VOLUME_TYPE_ENTERPRISE_WORKSPACE)
15667
15787
  original_node = self._otcs.get_node_by_volume_and_path(
15668
- volume_type=self._otcs.VOLUME_TYPE_ENTERPRISE_WORKSPACE,
15788
+ volume_type=original_volume,
15669
15789
  path=original_path,
15670
15790
  )
15671
15791
  original_id = self._otcs.get_result_value(
@@ -15759,14 +15879,23 @@ class Payload:
15759
15879
  )
15760
15880
  node_id = self._otcs.get_result_value(response=response, key="id")
15761
15881
  if not node_id:
15762
- self.logger.error("Failed to create item -> '%s'.", item_name)
15882
+ self.logger.error(
15883
+ "Failed to create item -> '%s' under parent%s.",
15884
+ item_name,
15885
+ " with nickname -> '{}'".format(parent_nickname)
15886
+ if parent_nickname
15887
+ else " path -> {} in volume -> {}".format(parent_path, parent_volume),
15888
+ )
15763
15889
  success = False
15764
15890
  continue
15765
15891
 
15766
15892
  self.logger.info(
15767
- "Successfully created item -> '%s' with ID -> %s.",
15893
+ "Successfully created item -> '%s' with ID -> %s under parent%s.",
15768
15894
  item_name,
15769
15895
  node_id,
15896
+ " with nickname -> '{}'".format(parent_nickname)
15897
+ if parent_nickname
15898
+ else " path -> {} in volume -> {}".format(parent_path, parent_volume),
15770
15899
  )
15771
15900
 
15772
15901
  # Special handling for scheduled bot items:
@@ -15829,14 +15958,20 @@ class Payload:
15829
15958
  success = False
15830
15959
  continue
15831
15960
 
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")
15961
+ # Check if we want to execute an action immediately after creation, like "Runnow":
15962
+ actions = item_details.get("actions", [])
15963
+ for action in actions:
15964
+ self.logger.info("Execute action -> '%s' for scheduled bot -> '%s'...", action, item_name)
15965
+ response = self._otcs.update_item(node_id=node_id, body=False, actionName=action)
15836
15966
  if not response:
15837
- self.logger.error("Failed to run scheduled bot item -> '%s'.", item_name)
15967
+ self.logger.error(
15968
+ "Failed to execute action -> '%s' for scheduled bot item -> '%s'.", action, item_name
15969
+ )
15838
15970
  success = False
15839
15971
  continue
15972
+ if not actions:
15973
+ self.logger.info("No immediate actions specified for scheduled bot -> '%s'.", item_name)
15974
+
15840
15975
  # end if item_type == self._otcs.ITEM_TYPE_SCHEDULED_BOT:
15841
15976
 
15842
15977
  # Special handling for collection items:
@@ -16035,7 +16170,7 @@ class Payload:
16035
16170
  user_id = self._otcs.get_result_value(response=response, key="id")
16036
16171
  if not user_id:
16037
16172
  self.logger.error(
16038
- "Cannot find user with name -> '%s'; cannot set user permissions. Skipping user...",
16173
+ "Cannot find user -> '%s'; cannot set user permissions. Skipping user...",
16039
16174
  user_name,
16040
16175
  )
16041
16176
  return False
@@ -16090,7 +16225,7 @@ class Payload:
16090
16225
  group_id = self._otcs.get_result_value(response=otcs_group, key="id")
16091
16226
  if not group_id:
16092
16227
  self.logger.error(
16093
- "Cannot find group with name -> '%s'; cannot set group permissions. Skipping group...",
16228
+ "Cannot find group -> '%s'; cannot set group permissions. Skipping group...",
16094
16229
  group_name,
16095
16230
  )
16096
16231
  return False
@@ -16149,7 +16284,7 @@ class Payload:
16149
16284
  )
16150
16285
  if not role_id:
16151
16286
  self.logger.error(
16152
- "Cannot find role with name -> '%s'; cannot set role permissions.",
16287
+ "Cannot find role -> '%s'; cannot set role permissions.",
16153
16288
  role_name,
16154
16289
  )
16155
16290
  return False
@@ -17473,6 +17608,11 @@ class Payload:
17473
17608
  continue
17474
17609
  workspace_type = doc_generator["workspace_type"]
17475
17610
 
17611
+ self._log_header_callback(
17612
+ text="Process Document Generator for workspace type -> '{}'".format(workspace_type),
17613
+ char="-",
17614
+ )
17615
+
17476
17616
  # Check if doc generator has been explicitly disabled in payload
17477
17617
  # (enabled = false). In this case we skip the element:
17478
17618
  if not doc_generator.get("enabled", True):
@@ -17715,7 +17855,7 @@ class Payload:
17715
17855
  )
17716
17856
  if response["results"]:
17717
17857
  self.logger.warning(
17718
- "Node with name -> '%s' does already exist in workspace folder with ID -> %s",
17858
+ "Node -> '%s' does already exist in workspace folder with ID -> %s",
17719
17859
  document_name,
17720
17860
  workspace_folder_id,
17721
17861
  )
@@ -17730,7 +17870,7 @@ class Payload:
17730
17870
  )
17731
17871
  if not response:
17732
17872
  self.logger.error(
17733
- "Failed to generate document -> '%s' in workspace -> '%s' (%s) as user -> %s",
17873
+ "Failed to generate document -> '%s' in workspace -> '%s' (%s) as user -> '%s'",
17734
17874
  document_name,
17735
17875
  workspace_name,
17736
17876
  workspace_id,
@@ -18288,10 +18428,9 @@ class Payload:
18288
18428
  # (enabled = false). In this case we skip this payload element:
18289
18429
  if not browser_automation.get("enabled", True):
18290
18430
  self.logger.info(
18291
- "Payload for %s automation -> '%s'%s is disabled. Skipping...",
18431
+ "Payload for %s automation -> '%s' is disabled. Skipping...",
18292
18432
  automation_type.lower(),
18293
18433
  name,
18294
- " ({})".format(description) if description else "",
18295
18434
  )
18296
18435
  continue
18297
18436
 
@@ -18300,6 +18439,7 @@ class Payload:
18300
18439
  self.logger.error(
18301
18440
  "%s automation -> '%s' is missing 'base_url' parameter. Skipping...", automation_type, name
18302
18441
  )
18442
+ browser_automation["result"] = "failure"
18303
18443
  success = False
18304
18444
  continue
18305
18445
 
@@ -18318,6 +18458,7 @@ class Payload:
18318
18458
  automation_type,
18319
18459
  name,
18320
18460
  )
18461
+ browser_automation["result"] = "failure"
18321
18462
  success = False
18322
18463
  continue
18323
18464
 
@@ -18343,18 +18484,19 @@ class Payload:
18343
18484
  headless=self._browser_headless,
18344
18485
  logger=self.logger,
18345
18486
  wait_until=wait_until,
18487
+ browser=browser_automation.get("browser"), # None is acceptable
18346
18488
  )
18347
- # Wait time is a global setting (for whole brwoser session)
18489
+ # Wait time is a global setting (for whole browser session)
18348
18490
  # This makes sure a page is fully loaded and elements are present
18349
- # before accessing them. We set 15.0 seconds as default if not
18491
+ # before accessing them. We set 30.0 seconds as default if not
18350
18492
  # otherwise specified by "wait_time" in the payload.
18351
- wait_time = browser_automation.get("wait_time", 15.0)
18493
+ wait_time = float(browser_automation.get("wait_time", 30.0))
18352
18494
  browser_automation_object.set_timeout(wait_time=wait_time)
18353
18495
  if "wait_time" in browser_automation:
18354
18496
  self.logger.info(
18355
18497
  "%s Automation wait time -> '%s' configured.",
18356
18498
  automation_type,
18357
- wait_time,
18499
+ str(wait_time),
18358
18500
  )
18359
18501
 
18360
18502
  # Initialize overall result status:
@@ -18362,11 +18504,17 @@ class Payload:
18362
18504
  first_step = True
18363
18505
 
18364
18506
  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)
18507
+ automation_step_type = automation_step.get("type", "")
18508
+ if not automation_step_type:
18509
+ self.logger.error(
18510
+ "%s automation step -> %s in browser automation -> '%s' is missing 'type' parameter. Stopping automation -> '%s'.",
18511
+ automation_type,
18512
+ str(automation_step),
18513
+ name,
18514
+ name,
18515
+ )
18367
18516
  success = False
18368
18517
  break
18369
- automation_step_type = automation_step.get("type", "")
18370
18518
  dependent = automation_step.get("dependent", True)
18371
18519
  if not dependent and not result:
18372
18520
  self.logger.warning(
@@ -18384,7 +18532,7 @@ class Payload:
18384
18532
  )
18385
18533
  continue
18386
18534
  elif not first_step:
18387
- self.logger.info(
18535
+ self.logger.debug(
18388
18536
  "Current step -> '%s' is %s on proceeding step.",
18389
18537
  automation_step_type,
18390
18538
  "dependent" if dependent else "not dependent",
@@ -18422,6 +18570,7 @@ class Payload:
18422
18570
  "Cannot log into -> %s. Skipping to next automation step...",
18423
18571
  base_url + page,
18424
18572
  )
18573
+ automation_step["result"] = "failure"
18425
18574
  success = False
18426
18575
  continue
18427
18576
  self.logger.info(
@@ -18436,6 +18585,7 @@ class Payload:
18436
18585
  "Automation step type -> '%s' requires 'page' parameter. Stopping automation.",
18437
18586
  automation_step_type,
18438
18587
  )
18588
+ automation_step["result"] = "failure"
18439
18589
  success = False
18440
18590
  break
18441
18591
  volume = automation_step.get("volume", OTCS.VOLUME_TYPE_ENTERPRISE_WORKSPACE)
@@ -18454,6 +18604,7 @@ class Payload:
18454
18604
  automation_type,
18455
18605
  name,
18456
18606
  )
18607
+ automation_step["result"] = "failure"
18457
18608
  success = False
18458
18609
  continue
18459
18610
  self.logger.info(
@@ -18479,6 +18630,7 @@ class Payload:
18479
18630
  "Cannot load page -> %s. Skipping this step...",
18480
18631
  page,
18481
18632
  )
18633
+ automation_step["result"] = "failure"
18482
18634
  success = False
18483
18635
  continue
18484
18636
  self.logger.info(
@@ -18494,17 +18646,22 @@ class Payload:
18494
18646
  "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18495
18647
  automation_step_type,
18496
18648
  )
18649
+ automation_step["result"] = "failure"
18497
18650
  success = False
18498
18651
  break
18499
18652
  # We keep the deprecated "find" syntax supported (for now)
18500
18653
  selector_type = automation_step.get("selector_type", automation_step.get("find", "id"))
18501
18654
  show_error = automation_step.get("show_error", True)
18655
+ # Do we navigate away from the current page with the click?
18502
18656
  navigation = automation_step.get("navigation", False)
18657
+ # Do we open a new browser (popup) window with the click?
18658
+ popup_window = automation_step.get("popup_window", False)
18659
+ # De we close the current (popup) window with the click?
18660
+ close_window = automation_step.get("close_window", False)
18661
+ # Do we have a 'desired' state for clicking a checkbox?
18503
18662
  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
18663
  wait_until = automation_step.get("wait_until", None)
18664
+ wait_time = automation_step.get("wait_time", 0.0)
18508
18665
  role_type = automation_step.get("role_type", None)
18509
18666
  result = browser_automation_object.find_elem_and_click(
18510
18667
  selector=selector,
@@ -18512,7 +18669,10 @@ class Payload:
18512
18669
  role_type=role_type,
18513
18670
  desired_checkbox_state=checkbox_state,
18514
18671
  is_navigation_trigger=navigation,
18672
+ is_popup_trigger=popup_window,
18673
+ is_page_close_trigger=close_window,
18515
18674
  wait_until=wait_until,
18675
+ wait_time=wait_time,
18516
18676
  show_error=show_error,
18517
18677
  )
18518
18678
  if not result:
@@ -18521,15 +18681,17 @@ class Payload:
18521
18681
  )
18522
18682
  if show_error:
18523
18683
  self.logger.error(message)
18684
+ automation_step["result"] = "failure"
18524
18685
  success = False
18525
18686
  else:
18526
18687
  self.logger.warning(message)
18527
18688
  continue
18528
18689
  self.logger.info(
18529
- "Successfully clicked %s element selected by -> '%s' (%s)",
18690
+ "Successfully clicked %s element selected by -> '%s' (%s%s)",
18530
18691
  "navigational" if navigation else "non-navigational",
18531
18692
  selector,
18532
- selector_type,
18693
+ "selector type -> '{}'".format(selector_type),
18694
+ ", role type -> '{}'".format(role_type) if role_type else "",
18533
18695
  )
18534
18696
  case "set_elem":
18535
18697
  # We keep the deprecated "elem" syntax supported (for now)
@@ -18539,6 +18701,7 @@ class Payload:
18539
18701
  "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18540
18702
  automation_step_type,
18541
18703
  )
18704
+ automation_step["result"] = "failure"
18542
18705
  success = False
18543
18706
  break
18544
18707
  # We keep the deprecated "find" syntax supported (for now)
@@ -18552,11 +18715,13 @@ class Payload:
18552
18715
  selector,
18553
18716
  selector_type,
18554
18717
  )
18718
+ automation_step["result"] = "failure"
18555
18719
  success = False
18556
18720
  break
18557
18721
  # we also support replacing placeholders that are
18558
18722
  # enclosed in double % characters like %%OTCS_RESOURCE_ID%%:
18559
- value = self.replace_placeholders(value)
18723
+ if isinstance(value, str):
18724
+ value = self.replace_placeholders(value)
18560
18725
  show_error = automation_step.get("show_error", True)
18561
18726
  result = browser_automation_object.find_elem_and_set(
18562
18727
  selector=selector,
@@ -18566,19 +18731,24 @@ class Payload:
18566
18731
  show_error=show_error,
18567
18732
  )
18568
18733
  if not result:
18569
- message = "Cannot set element selected by -> '{}' ({}) to value -> '{}'. Skipping this step...".format(
18570
- selector, selector_type, value
18734
+ message = "Cannot set element selected by -> '{}' ({}{}) to value -> '{}'. Skipping this step...".format(
18735
+ selector,
18736
+ "selector type -> '{}'".format(selector_type),
18737
+ ", role type -> '{}'".format(role_type) if role_type else "",
18738
+ value,
18571
18739
  )
18572
18740
  if show_error:
18573
18741
  self.logger.error(message)
18742
+ automation_step["result"] = "failure"
18574
18743
  success = False
18575
18744
  else:
18576
18745
  self.logger.warning(message)
18577
18746
  continue
18578
18747
  self.logger.info(
18579
- "Successfully set element selected by -> '%s' (%s) to value -> '%s'.",
18748
+ "Successfully set element selected by -> '%s' (%s%s) to value -> '%s'.",
18580
18749
  selector,
18581
- selector_type,
18750
+ "selector type -> '{}'".format(selector_type),
18751
+ ", role type -> '{}'".format(role_type) if role_type else "",
18582
18752
  value,
18583
18753
  )
18584
18754
  case "check_elem":
@@ -18589,6 +18759,7 @@ class Payload:
18589
18759
  "Automation step type -> '%s' requires 'selector' parameter. Stopping automation.",
18590
18760
  automation_step_type,
18591
18761
  )
18762
+ automation_step["result"] = "failure"
18592
18763
  success = False
18593
18764
  break
18594
18765
  # We keep the deprecated "find" syntax supported (for now)
@@ -18613,20 +18784,33 @@ class Payload:
18613
18784
  substring=substring,
18614
18785
  min_count=min_count,
18615
18786
  wait_time=wait_time, # time to wait before the check is actually done
18787
+ show_error=not want_exist, # if element is not found that we do not want to find it is not an error
18616
18788
  )
18617
18789
  # Check if we didn't get what we want:
18618
18790
  if (not result and want_exist) or (result and not want_exist):
18619
18791
  self.logger.error(
18620
18792
  "%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),
18793
+ "Cannot find" if not result and want_exist else "Found",
18794
+ "{} elements with selector -> '{}' ({}{})".format(
18795
+ min_count if want_exist else count,
18796
+ selector,
18797
+ "selector type -> '{}'".format(selector_type),
18798
+ ", role type -> '{}'".format(role_type) if role_type else "",
18799
+ )
18800
+ if (min_count > 1 and want_exist) or (count > 1 and not want_exist)
18801
+ else "an element with selector -> '{}' ({}{})".format(
18802
+ selector,
18803
+ "selector type -> '{}'".format(selector_type),
18804
+ ", role type -> '{}'".format(role_type) if role_type else "",
18805
+ ),
18625
18806
  " with {}value -> '{}'".format("substring-" if substring else "", value)
18626
18807
  if value
18627
18808
  else "",
18628
18809
  " in attribute -> '{}'".format(attribute) if attribute else "",
18629
- " Found {}{} occurences.".format(count, " undesirable" if not want_exist else ""),
18810
+ " Found {}{} occurences.".format(
18811
+ count,
18812
+ " undesirable" if not want_exist else " from a minimum of {}".format(min_count),
18813
+ ),
18630
18814
  )
18631
18815
  success = False
18632
18816
  continue
@@ -18646,12 +18830,20 @@ class Payload:
18646
18830
  automation_step_type,
18647
18831
  automation_type.lower(),
18648
18832
  )
18833
+ automation_step["result"] = "failure"
18649
18834
  success = False
18650
18835
  break
18651
18836
  # end match automation_step_type:
18652
18837
  first_step = False
18653
18838
  # end for automation_step in automation_steps:
18839
+
18840
+ # Cleanup session and and remove reference to the object:
18654
18841
  browser_automation_object.end_session()
18842
+ browser_automation_object = None
18843
+
18844
+ browser_automation["result"] = (
18845
+ "failure" if any(step.get("result", "success") == "failure" for step in automation_steps) else "success"
18846
+ )
18655
18847
  # end for browser_automation in browser_automations:
18656
18848
 
18657
18849
  if check_status:
@@ -22053,7 +22245,7 @@ class Payload:
22053
22245
  if response["results"]:
22054
22246
  # We add the suffix with the key which should be unique:
22055
22247
  self.logger.warning(
22056
- "Workspace with name -> '%s' does already exist in folder with ID -> %s and we need to handle the name clash by using name -> '%s'",
22248
+ "Workspace -> '%s' does already exist in folder with ID -> %s and we need to handle the name clash by using name -> '%s'",
22057
22249
  workspace_name,
22058
22250
  parent_id,
22059
22251
  workspace_name + " (" + key + ")",
@@ -25151,7 +25343,7 @@ class Payload:
25151
25343
 
25152
25344
  # Create Business Relationship between workspace and sub-workspace:
25153
25345
  if workspace_id and sub_workspace_id:
25154
- # Check if workspace relationship does already exist in Extended ECM
25346
+ # Check if workspace relationship does already exist in OTCS
25155
25347
  # (this is an additional safety measure to avoid errors):
25156
25348
  response = self._otcs_frontend.get_workspace_relationships(
25157
25349
  workspace_id=workspace_id,
@@ -25870,7 +26062,7 @@ class Payload:
25870
26062
  else:
25871
26063
  # Case 4: no key given + name not found = item does not exist
25872
26064
  self.logger.info(
25873
- "No existing document with name -> '%s' in parent with ID -> %s",
26065
+ "Cannot find document -> '%s' in parent with ID -> %s",
25874
26066
  document_name,
25875
26067
  parent_id,
25876
26068
  )
@@ -25937,7 +26129,7 @@ class Payload:
25937
26129
 
25938
26130
  # We add the suffix with the key which should be unique:
25939
26131
  self.logger.warning(
25940
- "Document with name -> '%s' does already exist in workspace folder with ID -> %s and we need to handle the name clash and use name -> '%s'",
26132
+ "Document -> '%s' does already exist in workspace folder with ID -> %s and we need to handle the name clash and use name -> '%s'",
25941
26133
  document_name,
25942
26134
  parent_id,
25943
26135
  document_name + " (" + key + ")",
@@ -27113,7 +27305,8 @@ class Payload:
27113
27305
  )
27114
27306
  success = False
27115
27307
  continue
27116
- # end if key
27308
+ # end if key_attribute:
27309
+ # end if key:
27117
27310
  else:
27118
27311
  # If we haven't a key we try by parent + name
27119
27312
  response = self._otcs_frontend.get_node_by_parent_and_name(
@@ -27140,7 +27333,7 @@ class Payload:
27140
27333
  else:
27141
27334
  # Case 4: no key given + name not found = item does not exist
27142
27335
  self.logger.info(
27143
- "No existing item with name -> '%s' in parent with ID -> %s",
27336
+ "No existing item -> '%s' in parent with ID -> %s",
27144
27337
  item_name,
27145
27338
  parent_id,
27146
27339
  )
@@ -27207,7 +27400,7 @@ class Payload:
27207
27400
 
27208
27401
  # We add the suffix with the key which should be unique:
27209
27402
  self.logger.warning(
27210
- "Item with name -> '%s' does already exist in workspace folder with ID -> %s and we need to handle the name clash and use name -> '%s'",
27403
+ "Item -> '%s' does already exist in workspace folder with ID -> %s and we need to handle the name clash and use name -> '%s'",
27211
27404
  item_name,
27212
27405
  parent_id,
27213
27406
  item_name + " (" + key + ")",
@@ -27499,111 +27692,116 @@ class Payload:
27499
27692
 
27500
27693
  success: bool = True
27501
27694
 
27502
- self._avts.authenticate()
27503
-
27504
- for payload_repo in self._avts_repositories:
27505
- if not payload_repo.get("enabled", True):
27506
- continue
27507
-
27508
- repository = self._avts.get_repo_by_name(name=payload_repo["name"])
27509
-
27510
- if repository is None:
27511
- self.logger.info(
27512
- "Repository -> '%s' does not exist, creating it...",
27513
- payload_repo["name"],
27514
- )
27515
-
27516
- if payload_repo.get("type", "Extended ECM") == "Extended ECM":
27517
- repository = self._avts.create_extended_ecm_repo(
27518
- name=payload_repo["name"],
27519
- username=payload_repo["username"],
27520
- password=payload_repo["password"],
27521
- otcs_url=payload_repo["otcs_url"],
27522
- otcs_api_url=payload_repo["otcs_api_url"],
27523
- node_id=int(payload_repo["node_id"]),
27524
- )
27695
+ token = self._avts.authenticate()
27696
+ if not token:
27697
+ self.logger.error("Cannot authenticate at Aviator Search!")
27698
+ success = False
27699
+ else:
27700
+ for payload_repo in self._avts_repositories:
27701
+ if not payload_repo.get("enabled", True):
27702
+ continue
27525
27703
 
27526
- elif payload_repo["type"] == "Documentum":
27527
- self.logger.warning("Not yet implemented")
27528
- elif payload_repo["type"] == "MSTeams":
27529
- repository = self._avts.create_msteams_repo(
27530
- name=payload_repo["name"],
27531
- client_id=payload_repo["client_id"],
27532
- tenant_id=payload_repo["tenant_id"],
27533
- certificate_file=payload_repo["certificate_file"],
27534
- certificate_password=payload_repo["certificate_password"],
27535
- index_attachments=payload_repo.get("index_attachments", True),
27536
- index_call_recordings=payload_repo.get(
27537
- "index_call_recordings",
27538
- True,
27539
- ),
27540
- index_message_replies=payload_repo.get(
27541
- "index_message_replies",
27542
- True,
27543
- ),
27544
- index_user_chats=payload_repo.get("index_user_chats", True),
27545
- )
27546
- elif payload_repo["type"] == "SharePoint":
27547
- repository = self._avts.create_sharepoint_repo(
27548
- name=payload_repo["name"],
27549
- client_id=payload_repo["client_id"],
27550
- tenant_id=payload_repo["tenant_id"],
27551
- certificate_file=payload_repo["certificate_file"],
27552
- certificate_password=payload_repo["certificate_password"],
27553
- sharepoint_url=payload_repo["sharepoint_url"],
27554
- sharepoint_url_type=payload_repo["sharepoint_url_type"],
27555
- sharepoint_mysite_url=payload_repo["sharepoint_mysite_url"],
27556
- sharepoint_admin_url=payload_repo["sharepoint_admin_url"],
27557
- index_user_profiles=payload_repo.get(
27558
- "index_message_replies",
27559
- False,
27560
- ),
27561
- )
27562
- else:
27563
- self.logger.error(
27564
- "Invalid repository type -> '%s' specified. Valid values are: Extended ECM, Documentum, MSTeams, SharePoint",
27565
- payload_repo["type"],
27566
- )
27567
- success = False
27568
- break
27704
+ repository = self._avts.get_repo_by_name(name=payload_repo["name"])
27569
27705
 
27570
27706
  if repository is None:
27571
- self.logger.error(
27572
- "Creation of Aviator Search repository -> '%s' failed!",
27573
- payload_repo["name"],
27574
- )
27575
- success = False
27576
- else:
27577
27707
  self.logger.info(
27578
- "Successfully created Aviator Search repository -> '%s'",
27708
+ "Repository -> '%s' does not exist, creating it...",
27579
27709
  payload_repo["name"],
27580
27710
  )
27581
- self.logger.debug("%s", repository)
27582
27711
 
27583
- else:
27584
- self.logger.info(
27585
- "Aviator Search repository -> '%s' already exists.",
27586
- payload_repo["name"],
27587
- )
27712
+ if payload_repo.get("type", "Extended ECM") == "Extended ECM":
27713
+ repository = self._avts.create_extended_ecm_repo(
27714
+ name=payload_repo["name"],
27715
+ username=payload_repo["username"],
27716
+ password=payload_repo["password"],
27717
+ otcs_url=payload_repo["otcs_url"],
27718
+ otcs_api_url=payload_repo["otcs_api_url"],
27719
+ node_id=int(payload_repo["node_id"]),
27720
+ )
27588
27721
 
27589
- # Start Crawling
27590
- start_crawling = payload_repo.get("start", False)
27722
+ elif payload_repo["type"] == "Documentum":
27723
+ self.logger.warning("Not yet implemented")
27724
+ elif payload_repo["type"] == "MSTeams":
27725
+ repository = self._avts.create_msteams_repo(
27726
+ name=payload_repo["name"],
27727
+ client_id=payload_repo["client_id"],
27728
+ tenant_id=payload_repo["tenant_id"],
27729
+ certificate_file=payload_repo["certificate_file"],
27730
+ certificate_password=payload_repo["certificate_password"],
27731
+ index_attachments=payload_repo.get("index_attachments", True),
27732
+ index_call_recordings=payload_repo.get(
27733
+ "index_call_recordings",
27734
+ True,
27735
+ ),
27736
+ index_message_replies=payload_repo.get(
27737
+ "index_message_replies",
27738
+ True,
27739
+ ),
27740
+ index_user_chats=payload_repo.get("index_user_chats", True),
27741
+ )
27742
+ elif payload_repo["type"] == "SharePoint":
27743
+ repository = self._avts.create_sharepoint_repo(
27744
+ name=payload_repo["name"],
27745
+ client_id=payload_repo["client_id"],
27746
+ tenant_id=payload_repo["tenant_id"],
27747
+ certificate_file=payload_repo["certificate_file"],
27748
+ certificate_password=payload_repo["certificate_password"],
27749
+ sharepoint_url=payload_repo["sharepoint_url"],
27750
+ sharepoint_url_type=payload_repo["sharepoint_url_type"],
27751
+ sharepoint_mysite_url=payload_repo["sharepoint_mysite_url"],
27752
+ sharepoint_admin_url=payload_repo["sharepoint_admin_url"],
27753
+ index_user_profiles=payload_repo.get(
27754
+ "index_message_replies",
27755
+ False,
27756
+ ),
27757
+ )
27758
+ else:
27759
+ self.logger.error(
27760
+ "Invalid repository type -> '%s' specified. Valid values are: Extended ECM, Documentum, MSTeams, SharePoint",
27761
+ payload_repo["type"],
27762
+ )
27763
+ success = False
27764
+ break
27591
27765
 
27592
- if repository is not None and start_crawling:
27593
- response = self._avts.start_crawling(repo_name=payload_repo["name"])
27766
+ if repository is None:
27767
+ self.logger.error(
27768
+ "Creation of Aviator Search repository -> '%s' failed!",
27769
+ payload_repo["name"],
27770
+ )
27771
+ success = False
27772
+ else:
27773
+ self.logger.info(
27774
+ "Successfully created Aviator Search repository -> '%s'",
27775
+ payload_repo["name"],
27776
+ )
27777
+ self.logger.debug("%s", repository)
27594
27778
 
27595
- if response is None:
27596
- self.logger.error(
27597
- "Aviator Search start crawling on repository failed -> '%s'",
27598
- payload_repo["name"],
27599
- )
27600
- success = False
27601
27779
  else:
27602
27780
  self.logger.info(
27603
- "Aviator Search crawling started on repository -> '%s'",
27781
+ "Aviator Search repository -> '%s' already exists.",
27604
27782
  payload_repo["name"],
27605
27783
  )
27606
- self.logger.debug("%s", response)
27784
+
27785
+ # Start Crawling
27786
+ start_crawling = payload_repo.get("start", False)
27787
+
27788
+ if repository is not None and start_crawling:
27789
+ response = self._avts.start_crawling(repo_name=payload_repo["name"])
27790
+
27791
+ if response is None:
27792
+ self.logger.error(
27793
+ "Aviator Search start crawling on repository failed -> '%s'",
27794
+ payload_repo["name"],
27795
+ )
27796
+ success = False
27797
+ else:
27798
+ self.logger.info(
27799
+ "Aviator Search crawling started on repository -> '%s'",
27800
+ payload_repo["name"],
27801
+ )
27802
+ self.logger.debug("%s", response)
27803
+ # end for payload_repo in self._avts_repositories:
27804
+ # end else:
27607
27805
 
27608
27806
  self.write_status_file(
27609
27807
  success=success,
@@ -27646,26 +27844,28 @@ class Payload:
27646
27844
 
27647
27845
  success: bool = True
27648
27846
 
27649
- self._avts.authenticate()
27650
-
27651
27847
  if not self._avts_questions.get("enabled", True):
27652
27848
  self.logger.info(
27653
27849
  "Payload section -> '%s' is not enabled. Skipping...",
27654
27850
  section_name,
27655
27851
  )
27656
27852
  return True
27657
-
27658
27853
  questions = self._avts_questions.get("questions", [])
27659
27854
  self.logger.info("Sample questions -> %s", questions)
27660
27855
 
27661
- response = self._avts.set_questions(questions=questions)
27662
-
27663
- if response is None:
27664
- self.logger.error("Aviator Search setting questions failed")
27856
+ token = self._avts.authenticate()
27857
+ if not token:
27858
+ self.logger.error("Cannot authenticate at Aviator Search!")
27665
27859
  success = False
27666
27860
  else:
27667
- self.logger.info("Aviator Search questions set succesfully")
27668
- self.logger.debug("%s", response)
27861
+ response = self._avts.set_questions(questions=questions)
27862
+
27863
+ if response is None:
27864
+ self.logger.error("Aviator Search setting questions failed")
27865
+ success = False
27866
+ else:
27867
+ self.logger.info("Aviator Search questions set succesfully")
27868
+ self.logger.debug("%s", response)
27669
27869
 
27670
27870
  self.write_status_file(
27671
27871
  success=success,
@@ -29126,7 +29326,7 @@ class Payload:
29126
29326
  if response and response["results"]:
29127
29327
  # We add the suffix with the key which should be unique:
29128
29328
  self.logger.warning(
29129
- "Classification with name -> '%s' does already exist in folder with ID -> %s and we need to handle the name clash by using name -> '%s'",
29329
+ "Classification -> '%s' does already exist in folder with ID -> %s and we need to handle the name clash by using name -> '%s'",
29130
29330
  classification_name,
29131
29331
  parent_id,
29132
29332
  classification_name + " (" + key + ")",
@@ -29457,3 +29657,160 @@ class Payload:
29457
29657
  return bool(response)
29458
29658
 
29459
29659
  # end method definition
29660
+
29661
+ def process_nifi_flows(self, section_name: str = "nifi") -> bool:
29662
+ """Process Knowledge Discovery Nifi flows in payload and create them in Nifi.
29663
+
29664
+ Args:
29665
+ section_name (str, optional):
29666
+ The name of the payload section. It can be overridden
29667
+ for cases where multiple sections of same type
29668
+ are used (e.g. the "Post" sections).
29669
+ This name is also used for the "success" status
29670
+ files written to the Admin Personal Workspace.
29671
+
29672
+ Returns:
29673
+ bool:
29674
+ True, if payload has been processed without errors, False otherwise
29675
+
29676
+ """
29677
+
29678
+ if not self._nifi_flows:
29679
+ self.logger.info(
29680
+ "Payload section -> '%s' is empty. Skipping...",
29681
+ section_name,
29682
+ )
29683
+ return True
29684
+
29685
+ # If this payload section has been processed successfully before we
29686
+ # can return True and skip processing it once more:
29687
+ if self.check_status_file(payload_section_name=section_name):
29688
+ return True
29689
+
29690
+ success: bool = True
29691
+
29692
+ for nifi_flow in self._nifi_flows:
29693
+ if "name" not in nifi_flow:
29694
+ self.logger.error(
29695
+ "Knowledge Discovery Nifi flow needs a name! Skipping to next Nifi flow...",
29696
+ )
29697
+ success = False
29698
+ continue
29699
+ name = nifi_flow["name"]
29700
+
29701
+ # Check if element has been disabled in payload (enabled = false).
29702
+ # In this case we skip the element:
29703
+ if not nifi_flow.get("enabled", True):
29704
+ self.logger.info(
29705
+ "Payload for Knowledge Discovery Nifi flow -> '%s' is disabled. Skipping...",
29706
+ name,
29707
+ )
29708
+ continue
29709
+
29710
+ if "file" not in nifi_flow:
29711
+ self.logger.error(
29712
+ "Knowledge Discovery Nifi flow -> '%s' needs a file! Skipping to next Nifi flow...", name
29713
+ )
29714
+ success = False
29715
+ continue
29716
+ filename = nifi_flow["file"]
29717
+
29718
+ parameters = nifi_flow.get("parameters", [])
29719
+
29720
+ if not self._otkd:
29721
+ self.logger.error("Knowledge Discovery is not initialized. Stop processing Nifi flows.")
29722
+ success = False
29723
+ break
29724
+
29725
+ # Optional layout positions of the flow:
29726
+ position_x = nifi_flow.get("position_x", 0.0)
29727
+ position_y = nifi_flow.get("position_y", 0.0)
29728
+ start = nifi_flow.get("start", False)
29729
+
29730
+ self.logger.info("Processing Knowledge Discovery Nifi flow -> '%s'...", name)
29731
+
29732
+ existing = self._otkd.get_process_group_by_name(name=name)
29733
+ if existing:
29734
+ self.logger.warning("Nifi flow -> '%s' does already exist. Updating parameters only...", name)
29735
+ # We better don't start existing flows!? Otherwise this may produce errors.
29736
+ start = False
29737
+ else:
29738
+ response = self._otkd.upload_process_group(
29739
+ file_path=filename, name=name, position_x=position_x, position_y=position_y
29740
+ )
29741
+ if not response:
29742
+ self.logger.error("Failed to upload new Nifi flow -> '%s' for Knowledge Discovery!", name)
29743
+ success = False
29744
+ continue
29745
+ self.logger.info("Sucessfully uploaded new Nifi flow -> '%s' for Knowledge Discovery!", name)
29746
+
29747
+ for parameter in parameters:
29748
+ component = parameter.get("component", None)
29749
+ if not component:
29750
+ self.logger.error("Missing component in parameter of Nifi flow -> '%s'!", name)
29751
+ success = False
29752
+ continue
29753
+ parameter_name = parameter.get("name", None)
29754
+ if not parameter_name:
29755
+ self.logger.error(
29756
+ "Missing name in parameter of Nifi flow -> '%s', component -> '%s'!", name, component
29757
+ )
29758
+ success = False
29759
+ continue
29760
+ parameter_description = parameter.get("description", "")
29761
+ parameter_value = parameter.get("value", None)
29762
+ if not parameter_value:
29763
+ self.logger.error(
29764
+ "Missing value in parameter of Nifi flow -> '%s', component -> '%s'", name, component
29765
+ )
29766
+ success = False
29767
+ continue
29768
+ parameter_sensitive = parameter.get("sensitive", False)
29769
+
29770
+ response = self._otkd.update_parameter(
29771
+ component=component,
29772
+ parameter=parameter_name,
29773
+ value=parameter_value,
29774
+ sensitive=parameter_sensitive,
29775
+ description=parameter_description,
29776
+ )
29777
+ if not response:
29778
+ self.logger.error("Failed to update parameter -> '%s' of Nifi flow -> '%s'!", parameter_name, name)
29779
+ success = False
29780
+ continue
29781
+ self.logger.info(
29782
+ "Successfully updated parameter -> '%s' of component -> '%s' in Nifi flow -> '%s' to value -> '%s'.",
29783
+ parameter_name,
29784
+ component,
29785
+ name,
29786
+ parameter_value if not parameter_sensitive else "<sensitive>",
29787
+ )
29788
+ # end for parameter in parameters:
29789
+ if start:
29790
+ response = self._otkd.start_all_processors(name=name)
29791
+ if response:
29792
+ self.logger.info("Successfully started Nifi flow -> '%s'.", name)
29793
+ else:
29794
+ self.logger.error("Failed to start Nifi flow -> '%s'!", name)
29795
+ success = False
29796
+
29797
+ response = self._otkd.set_controller_services_state(name=name, state="ENABLED")
29798
+ if response:
29799
+ self.logger.info("Successfully enabled Nifi Controller Services for Nifi flow -> '%s'.", name)
29800
+ else:
29801
+ self.logger.error("Failed to enable Nifi Controller Services for Nifi flow -> '%s'!", name)
29802
+ success = False
29803
+
29804
+ else:
29805
+ self.logger.info("Don't (re)start Nifi flow -> '%s'.", name)
29806
+ # end for nifi_flow in self._nifi_flows:
29807
+
29808
+ self.write_status_file(
29809
+ success=success,
29810
+ payload_section_name=section_name,
29811
+ payload_section=self._nifi_flows,
29812
+ )
29813
+
29814
+ return success
29815
+
29816
+ # end method definition