pyxecm 0.0.17__py3-none-any.whl → 0.0.19__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/payload.py CHANGED
@@ -30,10 +30,18 @@ __init__ : class initializer
30
30
  replacePlaceholders: replace placeholder in admin config files
31
31
  initPayload: load and initialize the YAML payload
32
32
  getPayloadSection: delivers a section of the payload as a list of settings
33
+ getAllGroupNames: construct a list of all group name
34
+ checkStatusFile: check if the payload section has been processed before
35
+ writeStatusFile: Write a status file into the Admin Personal Workspace in Extended ECM
36
+ to indicate that the payload section has been deployed successfully
37
+ determineGroupID: determine the id of a group - either from payload or from OTCS
38
+ determineUserID: determine the id of a user - either from payload or from OTCS
39
+ determineWorkspaceID: determine the nodeID of a workspace - either from payload or from OTCS
33
40
 
34
41
  processPayload: process payload (main method)
35
42
  processWebHooks: process list of web hooks
36
43
  processPartitions: process the OTDS partitions
44
+ processPartitionLicenses: process the licenses that should be assigned to OTDS partitions (this includes existing partitions)
37
45
  processOAuthClients: process the OTDS OAuth clients
38
46
  processTrustedSites: process the OTDS trusted sites
39
47
  processSystemAttributes: process the OTDS system attributes
@@ -47,7 +55,6 @@ processTransportPackages: process Extended ECM transport packages
47
55
  processUserPhotos: process Extended ECM user photos (user profile)
48
56
  processUserPhotosM365: process user photos in payload and assign them to Microsoft 365 users.
49
57
  processWorkspaceTypes: process Extended ECM workspace types (needs to run after processTransportPackages)
50
- processWorkspaceTemplateRegistrations: register workspace templates as projects for Extended ECM for Engineering (deprecated)
51
58
  processWorkspaces: process Extended ECM workspace instances
52
59
  processWorkspaceRelationships: process Extended ECM workspace relationships
53
60
  processWorkspaceMembers: process Extended ECM workspace members (users and groups)
@@ -94,6 +101,8 @@ import logging
94
101
  import yaml
95
102
  import hcl2
96
103
  import re
104
+ import time
105
+ import json
97
106
 
98
107
  from urllib.parse import urlparse
99
108
 
@@ -101,6 +110,10 @@ from urllib.parse import urlparse
101
110
  import pyxecm.web as web
102
111
  import pyxecm.sap as sap
103
112
 
113
+ from pyxecm.otcs import OTCS
114
+ from pyxecm.otds import OTDS
115
+ from pyxecm.otac import OTAC
116
+
104
117
  logger = logging.getLogger(os.path.basename(__file__))
105
118
 
106
119
 
@@ -109,12 +122,12 @@ class Payload(object):
109
122
 
110
123
  # _debug controls whether or not transport processing is
111
124
  # stopped if one transport fails:
112
- _debug = False
113
- _otcs = None
114
- _otcs_backend = None
115
- _otcs_frontend = None
116
- _otac = None
117
- _otds = None
125
+ _debug: bool = False
126
+ _otcs: OTCS = None
127
+ _otcs_backend: OTCS = None
128
+ _otcs_frontend: OTCS = None
129
+ _otac: OTAC = None
130
+ _otds: OTDS = None
118
131
  _otiv = None
119
132
  _k8s = None
120
133
  _web = None
@@ -197,15 +210,18 @@ class Payload(object):
197
210
 
198
211
  _placeholder_values = {}
199
212
 
213
+ _otcs_restart_callback = None
214
+
200
215
  def __init__(
201
216
  self,
202
217
  payload_source: str,
203
218
  custom_settings_dir: str,
204
219
  k8s_object: object,
205
- otds_object: object,
206
- otac_object: object,
207
- otcs_backend_object: object,
208
- otcs_frontend_object: object,
220
+ otds_object: OTDS,
221
+ otac_object: OTAC,
222
+ otcs_backend_object: OTCS,
223
+ otcs_frontend_object: OTCS,
224
+ otcs_restart_callback: callable,
209
225
  otiv_object: object,
210
226
  m365_object: object,
211
227
  placeholder_values: dict = {},
@@ -216,10 +232,11 @@ class Payload(object):
216
232
  Args:
217
233
  payload_source (string): path or URL to payload source file
218
234
  k8s_object (object): Kubernetes object
219
- otds_object (object): OTDS object
220
- otac_object (object): OTAC object
221
- otcs_backend_object (object): OTCS backend object
222
- otcs_frontend_object (object): OTCS frontend object
235
+ otds_object (OTDS): OTDS object
236
+ otac_object (OTAC): OTAC object
237
+ otcs_backend_object (OTCS): OTCS backend object
238
+ otcs_frontend_object (OTCS): OTCS frontend object
239
+ otcs_restart_callback (callable): function to call if OTCS service needs a restart
223
240
  otiv_object (object): OTIV object
224
241
  m365_object (object): M365 object to talk to Microsoft Graph API
225
242
  placeholder_values (dict): dictionary of placeholder values to be replaced in admin settings
@@ -238,6 +255,7 @@ class Payload(object):
238
255
  self._m365 = m365_object
239
256
  self._custom_settings_dir = custom_settings_dir
240
257
  self._placeholder_values = placeholder_values
258
+ self._otcs_restart_callback = otcs_restart_callback
241
259
 
242
260
  self._http_object = web.HTTP()
243
261
 
@@ -357,9 +375,6 @@ class Payload(object):
357
375
  self._permissions = self.getPayloadSection("permissions")
358
376
  self._permissions_post = self.getPayloadSection("permissionsPost")
359
377
  self._assignments = self.getPayloadSection("assignments")
360
- self._workspace_template_registrations = self.getPayloadSection(
361
- "workspaceTemplateRegistrations",
362
- )
363
378
  self._security_clearances = self.getPayloadSection("securityClearances")
364
379
  self._supplemental_markings = self.getPayloadSection("supplementalMarkings")
365
380
  self._records_management_settings = self.getPayloadSection(
@@ -403,8 +418,269 @@ class Payload(object):
403
418
  # end method definition
404
419
 
405
420
  def getAllGroupNames(self) -> list:
421
+ """Construct a list of all group name
422
+
423
+ Returns:
424
+ list: list of all group names
425
+ """
406
426
  return [group.get("name") for group in self._groups]
407
427
 
428
+ # end method definition
429
+
430
+ def checkStatusFile(
431
+ self, payload_section_name: str, payload_specific: bool = True
432
+ ) -> bool:
433
+ """Check if the payload section has been processed before. This is
434
+ done by checking the existance of a text file in the Admin Personal
435
+ workspace in Extended ECM with the name of the payload section.
436
+
437
+ Args:
438
+ payload_section_name (str): name of the payload section. This
439
+ is used to construct the file name
440
+ payload_specific (bool): whether or not the success should be specific for
441
+ each payload file or if success is "global" - like for the deletion
442
+ of the existing M365 teams (which we don't want to execute per
443
+ payload file)
444
+ Returns:
445
+ bool: True if the payload has been processed successfully before, False otherwise
446
+ """
447
+
448
+ logger.info(
449
+ "Check if payload section -> {} has been processed successfully before...".format(
450
+ payload_section_name
451
+ )
452
+ )
453
+
454
+ response = self._otcs.getNodeByVolumeAndPath(
455
+ 142
456
+ ) # write to Personal Workspace of Admin
457
+ target_folder_id = self._otcs.getResultValue(response, "id")
458
+ if not target_folder_id:
459
+ target_folder_id = 2004 # use Personal Workspace of Admin as fallback
460
+
461
+ # Some sections are actually not payload specific like teamsM365Cleanup
462
+ # we don't want external payload runs to re-apply this processing:
463
+ if payload_specific:
464
+ file_name = os.path.basename(self._payload_source) # remove directories
465
+ file_name = os.path.splitext(file_name)[0] # remove file suffix
466
+ file_name = "success_" + file_name + "_" + payload_section_name + ".txt"
467
+ else:
468
+ file_name = "success_" + payload_section_name + ".txt"
469
+
470
+ status_document = self._otcs.getNodeByParentAndName(
471
+ parent_id=target_folder_id, name=file_name, show_error=False
472
+ )
473
+ if status_document and status_document["results"]:
474
+ name = self._otcs.getResultValue(status_document, "name")
475
+ if name == file_name:
476
+ logger.info(
477
+ "Payload section -> {} has been processed successfully before. Skipping...".format(
478
+ payload_section_name
479
+ )
480
+ )
481
+ return True
482
+ logger.info(
483
+ "Payload section -> {} has not been processed successfully before. Processing...".format(
484
+ payload_section_name
485
+ )
486
+ )
487
+ return False
488
+
489
+ # end method definition
490
+
491
+ def writeStatusFile(
492
+ self,
493
+ payload_section_name: str,
494
+ payload_section: list,
495
+ payload_specific: bool = True,
496
+ ) -> bool:
497
+ """Write a status file into the Admin Personal Workspace in Extended ECM
498
+ to indicate that the payload section has been deployed successfully.
499
+ This speeds up the customizing process in case the customizer pod
500
+ is restarted.
501
+
502
+ Args:
503
+ payload_section_name (str): name of the payload section
504
+ payload_section (list): payload section content - this is written as JSon into the file
505
+ payload_specific (bool): whether or not the success should be specific for
506
+ each payload file or if success is "global" - like for the deletion
507
+ of the existing M365 teams (which we don't want to execute per
508
+ payload file)
509
+ Returns:
510
+ bool: True if the status file as been upladed to Extended ECM successfully, False otherwise
511
+ """
512
+
513
+ response = self._otcs.getNodeByVolumeAndPath(
514
+ 142
515
+ ) # write to Personal Workspace of Admin (with Volume Type ID = 142)
516
+ target_folder_id = self._otcs.getResultValue(response, "id")
517
+ if not target_folder_id:
518
+ target_folder_id = 2004 # use Personal Workspace of Admin as fallback
519
+
520
+ # Some sections are actually not payload specific like teamsM365Cleanup
521
+ # we don't want external payload runs to re-apply this processing:
522
+ if payload_specific:
523
+ file_name = os.path.basename(self._payload_source) # remove directories
524
+ file_name = os.path.splitext(file_name)[0] # remove file suffix
525
+ file_name = "success_" + file_name + "_" + payload_section_name + ".txt"
526
+ else:
527
+ file_name = "success_" + payload_section_name + ".txt"
528
+ full_path = "/tmp/" + file_name
529
+
530
+ with open(full_path, mode="w") as localfile:
531
+ localfile.write(json.dumps(payload_section, indent=2))
532
+
533
+ response = self._otcs.uploadFileToParent(
534
+ file_url=full_path,
535
+ file_name=file_name,
536
+ mime_type="text/plain",
537
+ parent_id=target_folder_id,
538
+ )
539
+
540
+ if response:
541
+ logger.info(
542
+ "Payload section -> {} has been completed successfully!".format(
543
+ payload_section_name
544
+ )
545
+ )
546
+ return True
547
+
548
+ return False
549
+
550
+ # end method definition
551
+
552
+ def determineGroupID(self, group: dict) -> int:
553
+ """Determine the id of a group - either from payload or from OTCS.
554
+ If the group is found in OTCS write back the ID into the payload.
555
+
556
+ Args:
557
+ group (dict): group payload element
558
+
559
+ Returns:
560
+ int: group ID
561
+ Side Effects:
562
+ the group items are modified by adding an "id" dict element that
563
+ includes the technical ID of the group in Extended ECM
564
+ """
565
+
566
+ if "id" in group:
567
+ return group["id"]
568
+
569
+ if not "name" in group:
570
+ logger.error("Group needs a name to lookup the ID.")
571
+ return 0
572
+ group_name = group["name"]
573
+
574
+ existing_groups = self._otcs.getGroup(name=group_name)
575
+ if not existing_groups or not existing_groups["data"]:
576
+ logger.info(
577
+ "Cannot find an existing group with name -> {}".format(group_name)
578
+ )
579
+ return 0
580
+
581
+ # Get list of all matching groups:
582
+ existing_groups_list = existing_groups["data"]
583
+ # Find the group with the exact match of the name:
584
+ existing_group = next(
585
+ (item for item in existing_groups_list if item["name"] == group_name),
586
+ None,
587
+ )
588
+ # Have we found an exact match?
589
+ if existing_group:
590
+ group["id"] = existing_group["id"]
591
+ return group["id"]
592
+ else:
593
+ logger.info(
594
+ "Did not find an existing group with name -> {}".format(group_name)
595
+ )
596
+ return 0
597
+
598
+ # end method definition
599
+
600
+ def determineUserID(self, user: dict):
601
+ """Determine the id of a group - either from payload or from OTCS
602
+ If the user is found in OTCS write back the ID into the payload.
603
+
604
+ Args:
605
+ user (dict): user payload element
606
+
607
+ Returns:
608
+ int: user ID
609
+ Side Effects:
610
+ the user items are modified by adding an "id" dict element that
611
+ includes the technical ID of the user in Extended ECM
612
+ """
613
+
614
+ if "id" in user:
615
+ return user["id"]
616
+
617
+ if not "name" in user:
618
+ logger.error("User needs a login name to lookup the ID.")
619
+ return 0
620
+ user_name = user["name"]
621
+
622
+ existing_users = self._otcs.getUser(name=user_name)
623
+ if not existing_users or not existing_users["data"]:
624
+ logger.info(
625
+ "Cannot find an existing user with name -> {}".format(user_name)
626
+ )
627
+ return 0
628
+
629
+ # Get list of all matching users:
630
+ existing_users_list = existing_users["data"]
631
+ # Find the group with the exact match of the name:
632
+ existing_user = next(
633
+ (item for item in existing_users_list if item["name"] == user_name),
634
+ None,
635
+ )
636
+ # Have we found an exact match?
637
+ if existing_user:
638
+ user["id"] = existing_user["id"]
639
+ return user["id"]
640
+ else:
641
+ logger.info(
642
+ "Did not find an existing user with name -> {}".format(user_name)
643
+ )
644
+ return 0
645
+
646
+ # end method definition
647
+
648
+ def determineWorkspaceID(self, workspace: dict):
649
+ """Determine the nodeID of a workspace - either from payload or from OTCS"""
650
+
651
+ """Determine the id of a group - either from payload or from OTCS
652
+ If the user is found in OTCS write back the ID into the payload.
653
+
654
+ Args:
655
+ workspace (dict): workspace payload element
656
+
657
+ Returns:
658
+ int: workspace Node ID
659
+ Side Effects:
660
+ the workspace items are modified by adding an "nodeId" dict element that
661
+ includes the node ID of the workspace in Extended ECM
662
+ """
663
+
664
+ if "nodeId" in workspace:
665
+ return workspace["nodeId"]
666
+
667
+ response = self._otcs.getWorkspaceByTypeAndName(
668
+ workspace["type_name"], workspace["name"]
669
+ )
670
+ workspace_id = self._otcs.getResultValue(response, "id")
671
+ if workspace_id:
672
+ workspace["nodeId"] = workspace_id
673
+ return workspace_id
674
+ else:
675
+ logger.warning(
676
+ "Workspace of type -> {} and name -> {} does not exist.".format(
677
+ workspace["type_name"], workspace["name"]
678
+ )
679
+ )
680
+ return 0
681
+
682
+ # end method definition
683
+
408
684
  def processPayload(self):
409
685
  """Main method to process a payload file.
410
686
 
@@ -430,12 +706,16 @@ class Payload(object):
430
706
  logger.info(
431
707
  "========== Process Web Hooks (post) ===================="
432
708
  )
433
- self.processWebHooks(self._webhooks_post)
709
+ self.processWebHooks(self._webhooks_post, "webHooksPost")
434
710
  case "partitions":
435
711
  logger.info(
436
712
  "========== Process OTDS Partitions ====================="
437
713
  )
438
714
  self.processPartitions()
715
+ logger.info(
716
+ "========== Assign OTCS Licenses to Partitions =========="
717
+ )
718
+ self.processPartitionLicenses()
439
719
  case "oauthClients":
440
720
  logger.info(
441
721
  "========== Process OTDS OAuth Clients =================="
@@ -456,6 +736,11 @@ class Payload(object):
456
736
  "========== Process OTCS Groups ========================="
457
737
  )
458
738
  self.processGroups()
739
+ # Add all groups with ID the a lookup dict for placeholder replacements
740
+ # in adminSetting. This also updates the payload with group IDs from OTCS
741
+ # if the group already exists in Extended ECM. This is important especially
742
+ # if the customizer pod is restarted / run multiple times:
743
+ self.processGroupPlaceholders()
459
744
  if self._m365:
460
745
  logger.info(
461
746
  "========== Cleanup existing MS Teams ==================="
@@ -470,6 +755,11 @@ class Payload(object):
470
755
  "========== Process OTCS Users =========================="
471
756
  )
472
757
  self.processUsers()
758
+ # Add all users with ID the a lookup dict for placeholder replacements
759
+ # in adminSetting. This also updates the payload with user IDs from OTCS
760
+ # if the user already exists in Extended ECM. This is important especially
761
+ # if the cutomizer pod is restarted / run multiple times:
762
+ self.processUserPlaceholders()
473
763
  logger.info(
474
764
  "========== Assign OTCS Licenses to Users ==============="
475
765
  )
@@ -505,14 +795,16 @@ class Payload(object):
505
795
  self.processTeamsM365()
506
796
  case "adminSettings":
507
797
  logger.info(
508
- "========== Process OTCS LLConfig Settings (1) =========="
798
+ "========== Process Administration Settings (1) ========="
509
799
  )
510
800
  self.processAdminSettings(self._admin_settings)
511
801
  case "adminSettingsPost":
512
802
  logger.info(
513
- "========== Process OTCS LLConfig Settings (2) =========="
803
+ "========== Process Administration Settings (2) ========="
804
+ )
805
+ self.processAdminSettings(
806
+ self._admin_settings_post, "adminSettingsPost"
514
807
  )
515
- self.processAdminSettings(self._admin_settings_post)
516
808
  case "execPodCommands":
517
809
  logger.info(
518
810
  "========== Process Pod Commands ========================"
@@ -522,11 +814,15 @@ class Payload(object):
522
814
  logger.info(
523
815
  "========== Process CS Apps (backend) ==================="
524
816
  )
525
- self.processCSApplications()
817
+ self.processCSApplications(
818
+ self._otcs_backend, section_name="csApplicationsBackend"
819
+ )
526
820
  logger.info(
527
821
  "========== Process CS Apps (frontend) =================="
528
822
  )
529
- self.processCSApplications(self._otcs_frontend)
823
+ self.processCSApplications(
824
+ self._otcs_frontend, section_name="csApplicationsFrontend"
825
+ )
530
826
  case "externalSystems":
531
827
  logger.info(
532
828
  "========== Process External System Connections ========="
@@ -547,7 +843,9 @@ class Payload(object):
547
843
  logger.info(
548
844
  "========== Process Content Transport Packages =========="
549
845
  )
550
- self.processTransportPackages(self._content_transport_packages)
846
+ self.processTransportPackages(
847
+ self._content_transport_packages, "contentTransportPackages"
848
+ )
551
849
  case "transportPackagesPost":
552
850
  # if self._m365:
553
851
  # logger.info(
@@ -557,12 +855,9 @@ class Payload(object):
557
855
  logger.info(
558
856
  "========== Process Transport Packages (post) ==========="
559
857
  )
560
- self.processTransportPackages(self._transport_packages_post)
561
- case "workspaceTemplateRegistrations":
562
- logger.info(
563
- "========== Register Workspace Templates ================"
858
+ self.processTransportPackages(
859
+ self._transport_packages_post, "transportPackagesPost"
564
860
  )
565
- self.processWorkspaceTemplateRegistrations()
566
861
  case "workspaces":
567
862
  logger.info(
568
863
  "========== Process Workspaces =========================="
@@ -586,7 +881,8 @@ class Payload(object):
586
881
  (
587
882
  item
588
883
  for item in self._external_systems
589
- if item["external_system_type"] == "SAP"
884
+ if item.get("external_system_type")
885
+ and item["external_system_type"] == "SAP"
590
886
  ),
591
887
  None,
592
888
  )
@@ -615,7 +911,7 @@ class Payload(object):
615
911
  logger.info(
616
912
  "========== Process Web Reports (post) =================="
617
913
  )
618
- self.processWebReports(self._web_reports_post)
914
+ self.processWebReports(self._web_reports_post, "webReportsPost")
619
915
  case "additionalGroupMemberships":
620
916
  logger.info(
621
917
  "========== Process additional group members for OTDS ==="
@@ -640,7 +936,7 @@ class Payload(object):
640
936
  logger.info(
641
937
  "========== Process Items (post) ========================"
642
938
  )
643
- self.processItems(self._items_post)
939
+ self.processItems(self._items_post, "itemsPost")
644
940
  case "permissions":
645
941
  logger.info(
646
942
  "========== Process Permissions ========================="
@@ -687,6 +983,23 @@ class Payload(object):
687
983
  payload_section["name"]
688
984
  )
689
985
  )
986
+ payload_section_restart = payload_section.get("restart", False)
987
+ if payload_section_restart:
988
+ logger.info(
989
+ "Payload section -> {} requests a restart of OTCS services...".format(
990
+ payload_section["name"]
991
+ )
992
+ )
993
+ # Restart OTCS frontend and backend pods:
994
+ self._otcs_restart_callback(self._otcs_backend, self._k8s)
995
+ # give some additional time to make sure service is responsive
996
+ time.sleep(30)
997
+ else:
998
+ logger.info(
999
+ "Payload section -> {} does not require a restart of OTCS services".format(
1000
+ payload_section["name"]
1001
+ )
1002
+ )
690
1003
 
691
1004
  if self._users:
692
1005
  logger.info("========== Process User Profile Photos =================")
@@ -701,22 +1014,35 @@ class Payload(object):
701
1014
 
702
1015
  # end method definition
703
1016
 
704
- def processWebHooks(self, webhooks: list):
1017
+ def processWebHooks(self, webhooks: list, section_name: str = "webHooks") -> bool:
705
1018
  """Process Web Hooks in payload and do HTTP requests.
706
1019
 
707
1020
  Args:
708
1021
  None
709
1022
  Returns:
710
- None
1023
+ bool: True if payload has been processed without errors, False otherwise
711
1024
  """
712
1025
 
713
1026
  if not webhooks:
714
- return None
1027
+ logger.info(
1028
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1029
+ )
1030
+ return True
1031
+
1032
+ # if this payload section has been processed successfully before we can return True
1033
+ # and skip processing once more
1034
+
1035
+ # WE LET THIS RUN EACH TIME!
1036
+ # if self.checkStatusFile(section_name):
1037
+ # return True
1038
+
1039
+ success: bool = True
715
1040
 
716
1041
  for webhook in webhooks:
717
1042
  url = webhook.get("url")
718
1043
  if not url:
719
1044
  logger.info("Web Hook does not have a url - skipping...")
1045
+ success = False
720
1046
  continue
721
1047
 
722
1048
  # Check if element has been disabled in payload (enabled = false).
@@ -744,21 +1070,40 @@ class Payload(object):
744
1070
 
745
1071
  self._http_object.httpRequest(url, method, payload, headers)
746
1072
 
1073
+ # if success:
1074
+ # self.writeStatusFile(section_name, webhooks)
1075
+
1076
+ return success
1077
+
747
1078
  # end method definition
748
1079
 
749
- def processPartitions(self):
1080
+ def processPartitions(self, section_name: str = "partitions") -> bool:
750
1081
  """Process OTDS partitions in payload and create them in OTDS.
751
1082
 
752
1083
  Args:
753
1084
  None
754
1085
  Returns:
755
- None
1086
+ bool: True if payload has been processed without errors, False otherwise
756
1087
  """
757
1088
 
1089
+ if not self._partitions:
1090
+ logger.info(
1091
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1092
+ )
1093
+ return True
1094
+
1095
+ # if this payload section has been processed successfully before we can return True
1096
+ # and skip processing once more
1097
+ if self.checkStatusFile(section_name):
1098
+ return True
1099
+
1100
+ success: bool = True
1101
+
758
1102
  for partition in self._partitions:
759
1103
  partition_name = partition.get("name")
760
1104
  if not partition_name:
761
1105
  logger.error("Partition does not have a name - skipping...")
1106
+ success = False
762
1107
  continue
763
1108
 
764
1109
  # Check if element has been disabled in payload (enabled = false).
@@ -800,6 +1145,7 @@ class Payload(object):
800
1145
  logger.error(
801
1146
  "Failed to add OTDS partition -> {}".format(partition_name)
802
1147
  )
1148
+ success = False
803
1149
  continue
804
1150
 
805
1151
  access_role = partition.get("access_role")
@@ -819,6 +1165,7 @@ class Payload(object):
819
1165
  partition_name, access_role
820
1166
  )
821
1167
  )
1168
+ success = False
822
1169
  continue
823
1170
 
824
1171
  # Partions may have an optional list of licenses in
@@ -832,9 +1179,10 @@ class Payload(object):
832
1179
  logger.error(
833
1180
  "Cannot find OTCS resource -> {}".format(otcs_resource_name)
834
1181
  )
1182
+ success = False
835
1183
  continue
836
1184
  otcs_resource_id = otcs_resource["resourceID"]
837
- license_name = "Extended ECM"
1185
+ license_name = "EXTENDED_ECM"
838
1186
  for license_feature in partition_specific_licenses:
839
1187
  assigned_license = self._otds.assignPartitionToLicense(
840
1188
  partition_name,
@@ -849,6 +1197,7 @@ class Payload(object):
849
1197
  partition_name, license_feature, license_name
850
1198
  )
851
1199
  )
1200
+ success = False
852
1201
  else:
853
1202
  logger.info(
854
1203
  "Successfully assigned partition -> {} to license feature -> {} of license -> {}".format(
@@ -856,21 +1205,144 @@ class Payload(object):
856
1205
  )
857
1206
  )
858
1207
 
1208
+ if success:
1209
+ self.writeStatusFile(section_name, self._partitions)
1210
+
1211
+ return success
1212
+
859
1213
  # end method definition
860
1214
 
861
- def processOAuthClients(self):
862
- """Process OTDS OAuth clients in payload and create them in OTDS.
1215
+ def processPartitionLicenses(self, section_name: str = "partitionLicenses") -> bool:
1216
+ """Process the licenses that should be assigned to OTDS partitions
1217
+ (this includes existing partitions).
863
1218
 
864
1219
  Args:
865
1220
  None
866
1221
  Returns:
1222
+ bool: True if payload has been processed without errors, False otherwise
1223
+ """
1224
+
1225
+ if not self._partitions:
1226
+ logger.info(
1227
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1228
+ )
1229
+ return True
1230
+
1231
+ # if this payload section has been processed successfully before we can return True
1232
+ # and skip processing once more
1233
+ if self.checkStatusFile(section_name):
1234
+ return True
1235
+
1236
+ success: bool = True
1237
+
1238
+ for partition in self._partitions:
1239
+ partition_name = partition.get("name")
1240
+ if not partition_name:
1241
+ logger.error("Partition does not have a name - skipping...")
1242
+ success = False
1243
+ continue
1244
+
1245
+ # Check if element has been disabled in payload (enabled = false).
1246
+ # In this case we skip the element:
1247
+ if "enabled" in partition and not partition["enabled"]:
1248
+ logger.info(
1249
+ "Payload for Partition -> {} is disabled. Skipping...".format(
1250
+ partition_name
1251
+ )
1252
+ )
1253
+ continue
1254
+
1255
+ response = self._otds.getPartition(partition_name, show_error=True)
1256
+ if not response:
1257
+ logger.error(
1258
+ "Partition -> {} does not exist. Skipping...".format(partition_name)
1259
+ )
1260
+ success = False
1261
+ continue
1262
+
1263
+ # Partions may have an optional list of licenses in
1264
+ # the payload. Assign the partition to all these licenses:
1265
+ partition_specific_licenses = partition.get("licenses")
1266
+ if partition_specific_licenses:
1267
+ # We assume these licenses are Extended ECM licenses!
1268
+ otcs_resource_name = self._otcs.config()["resource"]
1269
+ otcs_resource = self._otds.getResource(otcs_resource_name)
1270
+ if not otcs_resource:
1271
+ logger.error(
1272
+ "Cannot find OTCS resource -> {}".format(otcs_resource_name)
1273
+ )
1274
+ success = False
1275
+ continue
1276
+ otcs_resource_id = otcs_resource["resourceID"]
1277
+ license_name = "EXTENDED_ECM"
1278
+ for license_feature in partition_specific_licenses:
1279
+ if self._otds.isPartitionLicensed(
1280
+ partition_name=partition_name,
1281
+ resource_id=otcs_resource_id,
1282
+ license_feature=license_feature,
1283
+ license_name=license_name,
1284
+ ):
1285
+ logger.info(
1286
+ "Partition -> {} is already licensed for -> {} ({})".format(
1287
+ partition_name, license_name, license_feature
1288
+ )
1289
+ )
1290
+ continue
1291
+ assigned_license = self._otds.assignPartitionToLicense(
1292
+ partition_name,
1293
+ otcs_resource_id,
1294
+ license_feature,
1295
+ license_name,
1296
+ )
1297
+
1298
+ if not assigned_license:
1299
+ logger.error(
1300
+ "Failed to assign partition -> {} to license feature -> {} of license -> {}!".format(
1301
+ partition_name, license_feature, license_name
1302
+ )
1303
+ )
1304
+ success = False
1305
+ else:
1306
+ logger.info(
1307
+ "Successfully assigned partition -> {} to license feature -> {} of license -> {}".format(
1308
+ partition_name, license_feature, license_name
1309
+ )
1310
+ )
1311
+
1312
+ if success:
1313
+ self.writeStatusFile(section_name, self._partitions)
1314
+
1315
+ return success
1316
+
1317
+ # end method definition
1318
+
1319
+ def processOAuthClients(self, section_name: str = "oauthClients") -> bool:
1320
+ """Process OTDS OAuth clients in payload and create them in OTDS.
1321
+
1322
+ Args:
867
1323
  None
1324
+ Returns:
1325
+ bool: True if payload has been processed without errors, False otherwise
868
1326
  """
869
1327
 
1328
+ if not self._oauth_clients:
1329
+ logger.info(
1330
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1331
+ )
1332
+ return True
1333
+
1334
+ # if this payload section has been processed successfully before we can return True
1335
+ # and skip processing once more
1336
+ if self.checkStatusFile(section_name):
1337
+ return True
1338
+
1339
+ success: bool = True
1340
+
870
1341
  for oauth_client in self._oauth_clients:
871
1342
  client_name = oauth_client.get("name")
872
1343
  if not client_name:
873
1344
  logger.error("OAuth client does not have a name - skipping...")
1345
+ success = False
874
1346
  continue
875
1347
 
876
1348
  # Check if element has been disabled in payload (enabled = false).
@@ -929,6 +1401,7 @@ class Payload(object):
929
1401
  logger.error(
930
1402
  "Failed to add OTDS OAuth client -> {}".format(client_name)
931
1403
  )
1404
+ success = False
932
1405
  continue
933
1406
 
934
1407
  client_secret = response.get("secret")
@@ -943,83 +1416,241 @@ class Payload(object):
943
1416
  client_name, {"description": client_description}
944
1417
  )
945
1418
 
1419
+ if success:
1420
+ self.writeStatusFile(section_name, self._oauth_clients)
1421
+
1422
+ return success
1423
+
946
1424
  # self._otds.addOauthClientsToAccessRole()
947
1425
 
948
1426
  # end method definition
949
1427
 
950
- def processTrustedSites(self):
1428
+ def processTrustedSites(self, section_name: str = "trustedSites") -> bool:
951
1429
  """Process OTDS trusted sites in payload and create them in OTDS.
952
1430
 
953
1431
  Args:
954
1432
  None
955
1433
  Returns:
956
- None
1434
+ bool: True if payload has been processed without errors, False otherwise
957
1435
  """
958
1436
 
1437
+ if not self._trusted_sites:
1438
+ logger.info(
1439
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1440
+ )
1441
+ return True
1442
+
1443
+ # if this payload section has been processed successfully before we can return True
1444
+ # and skip processing once more
1445
+ if self.checkStatusFile(section_name):
1446
+ return True
1447
+
1448
+ success: bool = True
1449
+
959
1450
  for trusted_site in self._trusted_sites:
960
- self._otds.addTrustedSite(trusted_site)
961
- logger.info("Added OTDS trusted site -> {}".format(trusted_site))
1451
+ response = self._otds.addTrustedSite(trusted_site)
1452
+ if response:
1453
+ logger.info("Added OTDS trusted site -> {}".format(trusted_site))
1454
+ else:
1455
+ logger.error("Failed to add trusted site -> {}".format(trusted_site))
1456
+ success = False
1457
+
1458
+ if success:
1459
+ self.writeStatusFile(section_name, self._trusted_sites)
1460
+
1461
+ return success
962
1462
 
963
1463
  # end method definition
964
1464
 
965
- def processSystemAttributes(self):
1465
+ def processSystemAttributes(self, section_name: str = "systemAttributes") -> bool:
966
1466
  """Process OTDS system attributes in payload and create them in OTDS.
967
1467
 
968
1468
  Args:
969
1469
  None
970
1470
  Returns:
971
- None
1471
+ bool: True if payload has been processed without errors, False otherwise
972
1472
  """
973
1473
 
1474
+ if not self._system_attributes:
1475
+ logger.info(
1476
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1477
+ )
1478
+ return True
1479
+
1480
+ # if this payload section has been processed successfully before we can return True
1481
+ # and skip processing once more
1482
+ if self.checkStatusFile(section_name):
1483
+ return True
1484
+
1485
+ success: bool = True
1486
+
974
1487
  for system_attribute in self._system_attributes:
975
1488
  # Check if there's a matching formal parameter defined on the Web Report node:
976
1489
  if not system_attribute.get("name"):
977
1490
  logger.error("OTDS System Attribute needs a name. Skipping...")
1491
+ success = False
978
1492
  continue
979
1493
  attribute_name = system_attribute["name"]
980
1494
 
981
- if "enabled" in system_attribute and not system_attribute["enabled"]:
1495
+ if "enabled" in system_attribute and not system_attribute["enabled"]:
1496
+ logger.info(
1497
+ "Payload for OTDS System Attribute -> {} is disabled. Skipping...".format(
1498
+ attribute_name
1499
+ )
1500
+ )
1501
+ continue
1502
+
1503
+ if not system_attribute.get("value"):
1504
+ logger.error("OTDS System Attribute needs a value. Skipping...")
1505
+ continue
1506
+
1507
+ attribute_value = system_attribute["value"]
1508
+ attribute_description = system_attribute.get("description")
1509
+ response = self._otds.addSystemAttribute(
1510
+ attribute_name, attribute_value, attribute_description
1511
+ )
1512
+ if response:
1513
+ logger.info(
1514
+ "Added OTDS system attribute -> {} with value -> {}".format(
1515
+ attribute_name, attribute_value
1516
+ )
1517
+ )
1518
+ else:
1519
+ logger.error(
1520
+ "Failed to add OTDS system attribute -> {} with value -> {}".format(
1521
+ attribute_name, attribute_value
1522
+ )
1523
+ )
1524
+ success = False
1525
+
1526
+ if success:
1527
+ self.writeStatusFile(section_name, self._system_attributes)
1528
+
1529
+ return success
1530
+
1531
+ # end method definition
1532
+
1533
+ def processGroupPlaceholders(self):
1534
+ """For some adminSettings we may need to replace a placeholder (sourrounded by %%...%%)
1535
+ with the actual ID of the Extended ECM group. For this we prepare a lookup dict.
1536
+ The dict self._placeholder_values already includes lookups for the OTCS and OTAWP
1537
+ OTDS resource IDs (see main.py)
1538
+ """
1539
+
1540
+ for group in self._groups:
1541
+ if not "name" in group:
1542
+ logger.error(
1543
+ "Group needs a name for placeholder definition. Skipping..."
1544
+ )
1545
+ continue
1546
+ group_name = group["name"]
1547
+ # Check if group has been disabled in payload (enabled = false).
1548
+ # In this case we skip the element:
1549
+ if "enabled" in group and not group["enabled"]:
1550
+ logger.info(
1551
+ "Payload for Group -> {} is disabled. Skipping...".format(
1552
+ group_name
1553
+ )
1554
+ )
1555
+ continue
1556
+
1557
+ # Now we determine the ID. Either it is in the payload section from
1558
+ # the current customizer run or we try to look it up in the system.
1559
+ # The latter case may happen if the custiomuer pod got restarted.
1560
+ group_id = self.determineGroupID(group)
1561
+ if not group_id:
1562
+ logger.warning(
1563
+ "Group needs an ID for placeholder definition. Skipping..."
1564
+ )
1565
+ continue
1566
+
1567
+ # Add Group with its ID to the dict self._placeholder_values:
1568
+ self._placeholder_values[
1569
+ "OTCS_GROUP_ID_{}".format(
1570
+ group_name.upper().replace(" & ", "_").replace(" ", "_")
1571
+ )
1572
+ ] = str(group_id)
1573
+
1574
+ logger.debug(
1575
+ "Placeholder values after group processing = {}".format(
1576
+ self._placeholder_values
1577
+ )
1578
+ )
1579
+
1580
+ def processUserPlaceholders(self):
1581
+ """For some adminSettings we may need to replace a placeholder (sourrounded by %%...%%)
1582
+ with the actual ID of the Extended ECM user. For this we prepare a lookup dict.
1583
+ The dict self._placeholder_values already includes lookups for the OTCS and OTAWP
1584
+ OTDS resource IDs (see main.py)
1585
+ """
1586
+
1587
+ for user in self._users:
1588
+ if not "name" in user:
1589
+ logger.error(
1590
+ "User needs a name for placeholder definition. Skipping..."
1591
+ )
1592
+ continue
1593
+ user_name = user["name"]
1594
+ # Check if group has been disabled in payload (enabled = false).
1595
+ # In this case we skip the element:
1596
+ if "enabled" in user and not user["enabled"]:
982
1597
  logger.info(
983
- "Payload for OTDS System Attribute -> {} is disabled. Skipping...".format(
984
- attribute_name
985
- )
1598
+ "Payload for User -> {} is disabled. Skipping...".format(user_name)
986
1599
  )
987
1600
  continue
988
1601
 
989
- if not system_attribute.get("value"):
990
- logger.error("OTDS System Attribute needs a value. Skipping...")
1602
+ # Now we determine the ID. Either it is in the payload section from
1603
+ # the current customizer run or we try to look it up in the system.
1604
+ # The latter case may happen if the custiomuer pod got restarted.
1605
+ user_id = self.determineUserID(user)
1606
+ if not user_id:
1607
+ logger.warning(
1608
+ "User needs an ID for placeholder definition. Skipping..."
1609
+ )
991
1610
  continue
992
1611
 
993
- attribute_value = system_attribute["value"]
994
- attribute_description = system_attribute.get("description")
995
- self._otds.addSystemAttribute(
996
- attribute_name, attribute_value, attribute_description
997
- )
998
- logger.info(
999
- "Added OTDS system attribute -> {} with value -> {}".format(
1000
- attribute_name, attribute_value
1001
- )
1612
+ # Add Group with its ID to the dict self._placeholder_values:
1613
+ self._placeholder_values["OTCS_USER_ID_{}".format(user_name.upper())] = str(
1614
+ user_id
1002
1615
  )
1003
1616
 
1004
- # end method definition
1617
+ logger.debug(
1618
+ "Placeholder values after user processing = {}".format(
1619
+ self._placeholder_values
1620
+ )
1621
+ )
1005
1622
 
1006
- def processGroups(self):
1623
+ def processGroups(self, section_name: str = "groups") -> bool:
1007
1624
  """Process groups in payload and create them in Extended ECM.
1008
1625
 
1009
1626
  Args:
1010
1627
  None
1011
1628
  Returns:
1012
- None
1629
+ bool: True if payload has been processed without errors, False otherwise
1013
1630
  Side Effects:
1014
1631
  the group items are modified by adding an "id" dict element that
1015
1632
  includes the technical ID of the group in Extended ECM
1016
1633
  """
1017
1634
 
1635
+ if not self._groups:
1636
+ logger.info(
1637
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1638
+ )
1639
+ return True
1640
+
1641
+ # if this payload section has been processed successfully before we can return True
1642
+ # and skip processing once more
1643
+ if self.checkStatusFile(section_name):
1644
+ return True
1645
+
1646
+ success: bool = True
1647
+
1018
1648
  # First run through groups: create all groups in payload
1019
1649
  # and store the IDs of the created groups:
1020
1650
  for group in self._groups:
1021
1651
  if not "name" in group:
1022
1652
  logger.error("Group needs a name. Skipping...")
1653
+ success = False
1023
1654
  continue
1024
1655
  group_name = group["name"]
1025
1656
 
@@ -1035,70 +1666,35 @@ class Payload(object):
1035
1666
 
1036
1667
  # Check if the group does already exist (e.g. if job is restarted)
1037
1668
  # as this is a pattern search it could return multiple groups:
1038
- existing_groups = self._otcs.getGroup(group_name)
1039
- if existing_groups and existing_groups["data"]:
1040
- logger.debug(
1041
- "Found existing groups -> {}".format(existing_groups["data"])
1042
- )
1043
- # Get list of all matching groups:
1044
- existing_groups_list = existing_groups["data"]
1045
- # Find the group with the exact match of the name:
1046
- existing_group = next(
1047
- (
1048
- item
1049
- for item in existing_groups_list
1050
- if item["name"] == group_name
1051
- ),
1052
- None,
1053
- )
1054
- # Have we found an exact match?
1055
- if existing_group is not None:
1056
- logger.info(
1057
- "Found existing group -> {} ({}) - skipping to next group...".format(
1058
- existing_group["name"], existing_group["id"]
1059
- )
1669
+ group_id = self.determineGroupID(group)
1670
+ if group_id:
1671
+ logger.info(
1672
+ "Found existing group -> {} ({}) - skipping to next group...".format(
1673
+ group_name, group_id
1060
1674
  )
1061
- group["id"] = existing_group["id"]
1675
+ )
1676
+ continue
1677
+
1678
+ logger.info("Did not find an existing group - creating a new group...")
1062
1679
 
1063
- # Add Group with its ID to the class dict _placeholder_values:
1064
- self._placeholder_values[
1065
- "OTCS_GROUP_ID_{}".format(
1066
- group_name.upper().replace(" & ", "_").replace(" ", "_")
1067
- )
1068
- ] = str(group["id"])
1069
- continue
1070
- else:
1071
- logger.info(
1072
- "Did not find an exact match for the group - creating a new group..."
1073
- )
1074
- else:
1075
- logger.info("Did not find any matching group - creating a new group...")
1076
1680
  # Now we know it is a new group...
1077
1681
  new_group = self._otcs.addGroup(group_name)
1078
1682
  if new_group is not None:
1079
1683
  logger.debug("New group -> {}".format(new_group))
1080
1684
  group["id"] = new_group["id"]
1081
-
1082
- # Add Group with its ID to the global dict with placeholder values:
1083
- self._placeholder_values[
1084
- "OTCS_GROUP_ID_{}".format(
1085
- group_name.upper().replace(" & ", "_").replace(" ", "_")
1086
- )
1087
- ] = str(group["id"])
1685
+ else:
1686
+ logger.error("Failed to create group -> {}".format(new_group))
1687
+ success = False
1688
+ continue
1088
1689
 
1089
1690
  logger.debug("Groups = {}".format(self._groups))
1090
1691
 
1091
- logger.debug(
1092
- "Placeholder values after group processing = {}".format(
1093
- self._placeholder_values
1094
- )
1095
- )
1096
-
1097
1692
  # Second run through groups: create all group memberships
1098
1693
  # (nested groups) based on the IDs created in first run:
1099
1694
  for group in self._groups:
1100
1695
  if not "id" in group:
1101
1696
  logger.error("Group -> {} does not have an ID.".format(group["name"]))
1697
+ success = False
1102
1698
  continue
1103
1699
  parent_group_names = group["parent_groups"]
1104
1700
  for parent_group_name in parent_group_names:
@@ -1122,6 +1718,7 @@ class Payload(object):
1122
1718
  parent_group_name
1123
1719
  )
1124
1720
  )
1721
+ success = False
1125
1722
  continue
1126
1723
  parent_group = parent_group["data"][0]
1127
1724
  elif not "id" in parent_group:
@@ -1130,6 +1727,7 @@ class Payload(object):
1130
1727
  parent_group["name"]
1131
1728
  )
1132
1729
  )
1730
+ success = False
1133
1731
  continue
1134
1732
 
1135
1733
  # retrieve all members of the parent group (1 = get only groups)
@@ -1155,22 +1753,41 @@ class Payload(object):
1155
1753
  )
1156
1754
  self._otcs.addGroupMember(group["id"], parent_group["id"])
1157
1755
 
1756
+ if success:
1757
+ self.writeStatusFile(section_name, self._groups)
1758
+
1759
+ return success
1760
+
1158
1761
  # end method definition
1159
1762
 
1160
- def processGroupsM365(self):
1763
+ def processGroupsM365(self, section_name: str = "groupsM365") -> bool:
1161
1764
  """Process groups in payload and create them in Microsoft 365.
1162
1765
 
1163
1766
  Args:
1164
1767
  None
1165
1768
  Returns:
1166
- None
1769
+ bool: True if payload has been processed without errors, False otherwise
1167
1770
  """
1168
1771
 
1772
+ if not self._groups:
1773
+ logger.info(
1774
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1775
+ )
1776
+ return True
1777
+
1778
+ # if this payload section has been processed successfully before we can return True
1779
+ # and skip processing once more
1780
+ if self.checkStatusFile(section_name):
1781
+ return True
1782
+
1783
+ success: bool = True
1784
+
1169
1785
  # First run through groups: create all groups in payload
1170
1786
  # and store the IDs of the created groups:
1171
1787
  for group in self._groups:
1172
1788
  if not "name" in group:
1173
1789
  logger.error("Group needs a name. Skipping...")
1790
+ success = False
1174
1791
  continue
1175
1792
  group_name = group["name"]
1176
1793
 
@@ -1239,27 +1856,48 @@ class Payload(object):
1239
1856
  group_name, group["m365_id"]
1240
1857
  )
1241
1858
  )
1859
+ else:
1860
+ success = False
1861
+
1862
+ if success:
1863
+ self.writeStatusFile(section_name, self._groups)
1864
+
1865
+ return success
1242
1866
 
1243
1867
  # end method definition
1244
1868
 
1245
- def processUsers(self):
1869
+ def processUsers(self, section_name: str = "users") -> bool:
1246
1870
  """Process users in payload and create them in Extended ECM.
1247
1871
 
1248
1872
  Args:
1249
1873
  None
1250
1874
  Returns:
1251
- None
1875
+ bool: True if payload has been processed without errors, False otherwise
1252
1876
  Side Effects:
1253
1877
  the user items are modified by adding an "id" dict element that
1254
1878
  includes the technical ID of the user in Extended ECM
1255
1879
  """
1256
1880
 
1881
+ if not self._users:
1882
+ logger.info(
1883
+ "Payload section -> {} is empty. Skipping...".format(section_name)
1884
+ )
1885
+ return True
1886
+
1887
+ # if this payload section has been processed successfully before we can return True
1888
+ # and skip processing once more
1889
+ if self.checkStatusFile(section_name):
1890
+ return True
1891
+
1892
+ success: bool = True
1893
+
1257
1894
  # Add all users in payload and establish membership in
1258
1895
  # specified groups:
1259
1896
  for user in self._users:
1260
1897
  # Sanity checks:
1261
1898
  if not "name" in user:
1262
1899
  logger.error("User is missing a login - skipping to next user...")
1900
+ success = False
1263
1901
  continue
1264
1902
  user_name = user["name"]
1265
1903
 
@@ -1278,6 +1916,7 @@ class Payload(object):
1278
1916
  user_name
1279
1917
  )
1280
1918
  )
1919
+ success = False
1281
1920
  continue
1282
1921
 
1283
1922
  # Sanity checks:
@@ -1290,41 +1929,24 @@ class Payload(object):
1290
1929
  user["base_group"] = "DefaultGroup"
1291
1930
 
1292
1931
  # Check if the user does already exist (e.g. if job is restarted)
1293
- # As this is a pattern search it could return multiple users:
1294
- existing_users = self._otcs.getUser(user_name)
1295
- if existing_users and existing_users["data"]:
1296
- logger.debug(
1297
- "Found existing users -> {}".format(existing_users["data"])
1298
- )
1299
- # Get list of all matching users:
1300
- existing_users_list = existing_users["data"]
1301
- # Find the user with the exact match of the name:
1302
- existing_user = next(
1303
- (item for item in existing_users_list if item["name"] == user_name),
1304
- None,
1305
- )
1306
- # Have we found an exact match?
1307
- if existing_user is not None:
1308
- logger.info(
1309
- "Found existing user -> {} ({}) - skipping to next user...".format(
1310
- existing_user["name"], existing_user["id"]
1311
- )
1312
- )
1313
- user["id"] = existing_user["id"]
1314
- continue
1315
- else:
1316
- logger.info(
1317
- "Did not find an exact match for the user - creating a new user..."
1932
+ # determineUserID() also writes back the user ID into the payload
1933
+ # if it has gathered it from OTCS.
1934
+ user_id = self.determineUserID(user)
1935
+ if user_id:
1936
+ logger.info(
1937
+ "Found existing user -> {} ({}) - skipping to next user...".format(
1938
+ user_name, user_id
1318
1939
  )
1319
- else:
1320
- logger.info("Did not find any matching user - creating a new user...")
1940
+ )
1941
+ continue
1942
+ logger.info("Did not find an existing user - creating a new user...")
1321
1943
 
1322
1944
  # Find the base group of the user. Assume 'Default Group' (= 1001) if not found:
1323
1945
  base_group = next(
1324
1946
  (
1325
1947
  item["id"]
1326
1948
  for item in self._groups
1327
- if item["name"] == user["base_group"]
1949
+ if item["name"] == user["base_group"] and item.get("id")
1328
1950
  ),
1329
1951
  1001,
1330
1952
  )
@@ -1365,6 +1987,7 @@ class Payload(object):
1365
1987
  logger.error(
1366
1988
  "Group -> {} not found. Skipping...".format(group_name)
1367
1989
  )
1990
+ success = False
1368
1991
  continue
1369
1992
  group = group["data"][0]
1370
1993
 
@@ -1374,6 +1997,7 @@ class Payload(object):
1374
1997
  group["name"]
1375
1998
  )
1376
1999
  )
2000
+ success = False
1377
2001
  continue
1378
2002
 
1379
2003
  logger.info(
@@ -1381,10 +2005,14 @@ class Payload(object):
1381
2005
  user["name"], user["id"], group["name"], group["id"]
1382
2006
  )
1383
2007
  )
1384
- self._otcs.addGroupMember(user["id"], group["id"])
2008
+ response = self._otcs.addGroupMember(user["id"], group["id"])
2009
+ if not response:
2010
+ success = False
1385
2011
  # for some unclear reason the user is not added to its base group in OTDS
1386
2012
  # so we do this explicitly:
1387
- self._otds.addUserToGroup(user["name"], user["base_group"])
2013
+ response = self._otds.addUserToGroup(user["name"], user["base_group"])
2014
+ if not response:
2015
+ success = False
1388
2016
 
1389
2017
  # Extra OTDS attributes for the user can be provided in "extra_attributes"
1390
2018
  # as part of the user payload.
@@ -1397,6 +2025,7 @@ class Payload(object):
1397
2025
  logger.error(
1398
2026
  "User attribute is missing a name or value. Skipping..."
1399
2027
  )
2028
+ success = False
1400
2029
  continue
1401
2030
  logger.info(
1402
2031
  "Set user attribute -> {} to -> {}".format(
@@ -1406,6 +2035,8 @@ class Payload(object):
1406
2035
  user_partition = self._otcs.config()["partition"]
1407
2036
  if not user_partition:
1408
2037
  logger.error("User partition not found!")
2038
+ success = False
2039
+ continue
1409
2040
  self._otds.updateUser(
1410
2041
  user_partition,
1411
2042
  user["name"],
@@ -1413,23 +2044,42 @@ class Payload(object):
1413
2044
  attribute_value,
1414
2045
  )
1415
2046
 
2047
+ if success:
2048
+ self.writeStatusFile(section_name, self._users)
2049
+
2050
+ return success
2051
+
1416
2052
  # end method definition
1417
2053
 
1418
- def processUsersM365(self):
2054
+ def processUsersM365(self, section_name: str = "usersM365") -> bool:
1419
2055
  """Process users in payload and create them in Microsoft 365 via MS Graph API.
1420
2056
 
1421
2057
  Args:
1422
2058
  None
1423
2059
  Returns:
1424
- None
2060
+ bool: True if payload has been processed without errors, False otherwise
1425
2061
  """
1426
2062
 
2063
+ if not self._users:
2064
+ logger.info(
2065
+ "Payload section -> {} is empty. Skipping...".format(section_name)
2066
+ )
2067
+ return True
2068
+
2069
+ # if this payload section has been processed successfully before we can return True
2070
+ # and skip processing once more
2071
+ if self.checkStatusFile(section_name):
2072
+ return True
2073
+
2074
+ success: bool = True
2075
+
1427
2076
  # Add all users in payload and establish membership in
1428
2077
  # specified groups:
1429
2078
  for user in self._users:
1430
2079
  # Sanity checks:
1431
2080
  if not "name" in user:
1432
2081
  logger.error("User is missing a login - skipping to next user...")
2082
+ success = False
1433
2083
  continue
1434
2084
  user_name = user["name"]
1435
2085
 
@@ -1455,6 +2105,7 @@ class Payload(object):
1455
2105
  user_name
1456
2106
  )
1457
2107
  )
2108
+ success = False
1458
2109
  continue
1459
2110
  user_password = user["password"]
1460
2111
  # be careful with the following fields - they could be empty
@@ -1504,6 +2155,7 @@ class Payload(object):
1504
2155
  user_name
1505
2156
  )
1506
2157
  )
2158
+ success = False
1507
2159
  continue
1508
2160
 
1509
2161
  # Now we assign a license to the new M365 user.
@@ -1524,6 +2176,7 @@ class Payload(object):
1524
2176
  sku_id, user_name
1525
2177
  )
1526
2178
  )
2179
+ success = False
1527
2180
  else:
1528
2181
  if (
1529
2182
  not "m365_skus" in user
@@ -1579,7 +2232,7 @@ class Payload(object):
1579
2232
  user_name, group_names, user_department
1580
2233
  )
1581
2234
  )
1582
- # Go through all
2235
+ # Go through all group names:
1583
2236
  for group_name in group_names:
1584
2237
  # Find the group payload item to the parent group name:
1585
2238
  group = next(
@@ -1620,6 +2273,7 @@ class Payload(object):
1620
2273
  group_name
1621
2274
  )
1622
2275
  )
2276
+ success = False
1623
2277
  else:
1624
2278
  group_id = response["value"][0]["id"]
1625
2279
 
@@ -1694,6 +2348,7 @@ class Payload(object):
1694
2348
  group_name
1695
2349
  )
1696
2350
  )
2351
+ success = False
1697
2352
  continue
1698
2353
  parent_group_id = response["value"][0]["id"]
1699
2354
 
@@ -1722,9 +2377,14 @@ class Payload(object):
1722
2377
  )
1723
2378
  self._m365.addGroupMember(parent_group_id, user_id)
1724
2379
 
2380
+ if success:
2381
+ self.writeStatusFile(section_name, self._users)
2382
+
2383
+ return success
2384
+
1725
2385
  # end method definition
1726
2386
 
1727
- def processTeamsM365(self):
2387
+ def processTeamsM365(self, section_name: str = "teamsM365") -> bool:
1728
2388
  """Process groups in payload and create matching Teams in Microsoft 365.
1729
2389
  We need to do this after the creation of the M365 users as wie require
1730
2390
  Group Owners to create teams.
@@ -1732,12 +2392,26 @@ class Payload(object):
1732
2392
  Args:
1733
2393
  None
1734
2394
  Returns:
1735
- None
2395
+ bool: True if payload has been processed without errors, False otherwise
1736
2396
  """
1737
2397
 
2398
+ if not self._groups:
2399
+ logger.info(
2400
+ "Payload section -> {} is empty. Skipping...".format(section_name)
2401
+ )
2402
+ return True
2403
+
2404
+ # if this payload section has been processed successfully before we can return True
2405
+ # and skip processing once more
2406
+ if self.checkStatusFile(section_name):
2407
+ return True
2408
+
2409
+ success: bool = True
2410
+
1738
2411
  for group in self._groups:
1739
2412
  if not "name" in group:
1740
2413
  logger.error("Team needs a name. Skipping...")
2414
+ success = False
1741
2415
  continue
1742
2416
  group_name = group["name"]
1743
2417
 
@@ -1766,6 +2440,7 @@ class Payload(object):
1766
2440
  group_name
1767
2441
  )
1768
2442
  )
2443
+ success = False
1769
2444
  continue
1770
2445
 
1771
2446
  if not self._m365.hasTeam(group_name):
@@ -1776,6 +2451,8 @@ class Payload(object):
1776
2451
  )
1777
2452
  # Now "upgrading" this group to a MS Team:
1778
2453
  new_team = self._m365.addTeam(group_name)
2454
+ if not new_team:
2455
+ success = False
1779
2456
  else:
1780
2457
  logger.info(
1781
2458
  "M365 group -> {} already has an MS Team connected. Skipping...".format(
@@ -1783,6 +2460,11 @@ class Payload(object):
1783
2460
  )
1784
2461
  )
1785
2462
 
2463
+ if success:
2464
+ self.writeStatusFile(section_name, self._groups)
2465
+
2466
+ return success
2467
+
1786
2468
  # end method definition
1787
2469
 
1788
2470
  def cleanupStaleTeamsM365(self, workspace_types: list):
@@ -1825,35 +2507,86 @@ class Payload(object):
1825
2507
 
1826
2508
  # end method definition
1827
2509
 
1828
- def cleanupAllTeamsM365(self) -> bool:
2510
+ def cleanupAllTeamsM365(self, section_name: str = "teamsM365Cleanup") -> bool:
1829
2511
  """Delete Microsoft Teams that are left-overs from former deployments
1830
2512
 
1831
2513
  Args:
1832
2514
  None
1833
2515
  Returns:
1834
- boolean: True if teams have been deleted, False otherwise
2516
+ bool: True if teams have been deleted, False otherwise
1835
2517
  """
1836
2518
 
2519
+ # We want this cleanup to only run once even if we have
2520
+ # multiple payload files - so we pass payload_specific=False here:
2521
+ if self.checkStatusFile(
2522
+ payload_section_name=section_name, payload_specific=False
2523
+ ):
2524
+ logger.info(
2525
+ "Payload section -> {} has been processed successfully before. Skip cleanup of M365 teams...".format(
2526
+ section_name
2527
+ )
2528
+ )
2529
+ return True
2530
+ else:
2531
+ logger.info("Processing payload section -> {}...".format(section_name))
2532
+
2533
+ # We don't want to delete MS Teams that are matching the regular OTCS Group Names (like "Sales")
1837
2534
  exception_list = self.getAllGroupNames()
1838
- pattern_list = [r"\(\d+\)", r"\d+\s-\s"]
2535
+
2536
+ # These are the patterns that each MS Teams needs to match at least one of to be deleted
2537
+ # Pattern 1: all MS teams with a name that has a number in brackets, line "(1234)"
2538
+ # Pattern 2: all MS Teams with a name that starts with a number followed by a space,
2539
+ # followed by a "- and followed by another space
2540
+ # Pattern 3: all MS Teams with a name that starts with "WS" and a 1-4 digit number
2541
+ # (these are the workspaces for Purchase Contracts generated for Intelligent Filing)
2542
+ # Pattern 4: all MS Teams with a name that ends with a 1-2 character + a number in brackets, like (US-1000)
2543
+ # this is a specialization of pattern 1
2544
+ pattern_list = [
2545
+ r"\(\d+\)",
2546
+ r"\d+\s-\s",
2547
+ r"^WS\d{1,4}$",
2548
+ r"^.+?\s\(.{1,2}-\d+\)$",
2549
+ ]
1839
2550
 
1840
2551
  result = self._m365.deleteAllTeams(exception_list, pattern_list)
1841
2552
 
2553
+ # We want this cleanup to only run once even if we have
2554
+ # multiple payload files - so we pass payload_specific=False here:
2555
+ self.writeStatusFile(
2556
+ payload_section_name=section_name,
2557
+ payload_section=exception_list + pattern_list,
2558
+ payload_specific=False,
2559
+ )
2560
+
1842
2561
  return result
1843
2562
 
1844
2563
  # end method definition
1845
2564
 
1846
- def processAdminSettings(self, admin_settings: list) -> bool:
2565
+ def processAdminSettings(
2566
+ self, admin_settings: list, section_name: str = "adminSettings"
2567
+ ) -> bool:
1847
2568
  """Process admin settings in payload and import them to Extended ECM.
1848
2569
 
1849
2570
  Args:
1850
2571
  admin_settings (list): list of admin settings. We need this parameter
1851
2572
  as we process two different lists.
1852
2573
  Returns:
1853
- boolean: True if a restart of the OTCS pods is required. False otherwise.
2574
+ bool: True if a restart of the OTCS pods is required. False otherwise.
1854
2575
  """
1855
2576
 
1856
- restart_required = False
2577
+ if not admin_settings:
2578
+ logger.info(
2579
+ "Payload section -> {} is empty. Skipping...".format(section_name)
2580
+ )
2581
+ return True
2582
+
2583
+ # if this payload section has been processed successfully before we can return True
2584
+ # and skip processing once more
2585
+ if self.checkStatusFile(section_name):
2586
+ return True
2587
+
2588
+ restart_required: bool = False
2589
+ success: bool = True
1857
2590
 
1858
2591
  for admin_setting in admin_settings:
1859
2592
  # Sanity checks:
@@ -1903,23 +2636,40 @@ class Payload(object):
1903
2636
  logger.error(
1904
2637
  "Admin settings file -> {} not found.".format(settings_file)
1905
2638
  )
2639
+ success = False
2640
+
2641
+ if success:
2642
+ self.writeStatusFile(section_name, admin_settings)
1906
2643
 
1907
2644
  return restart_required
1908
2645
 
1909
2646
  # end method definition
1910
2647
 
1911
- def processExternalSystems(self):
2648
+ def processExternalSystems(self, section_name: str = "externalSystems") -> bool:
1912
2649
  """Process external systems in payload and create them in Extended ECM.
1913
2650
 
1914
2651
  Args:
1915
2652
  None
1916
2653
  Returns:
1917
- None
2654
+ bool: True if payload has been processed without errors, False otherwise
1918
2655
  Side Effects:
1919
2656
  - based on system_type different other settings in the dict are set
1920
2657
  - reachability is tested and a flag is set in the dict are set
1921
2658
  """
1922
2659
 
2660
+ if not self._external_systems:
2661
+ logger.info(
2662
+ "Payload section -> {} is empty. Skipping...".format(section_name)
2663
+ )
2664
+ return True
2665
+
2666
+ # if this payload section has been processed successfully before we can return True
2667
+ # and skip processing once more
2668
+ if self.checkStatusFile(section_name):
2669
+ return True
2670
+
2671
+ success: bool = True
2672
+
1923
2673
  for external_system in self._external_systems:
1924
2674
  #
1925
2675
  # 1: Do sanity checks for the payload:
@@ -1928,6 +2678,7 @@ class Payload(object):
1928
2678
  logger.error(
1929
2679
  "External System connection needs a logical system name! Skipping to next external system..."
1930
2680
  )
2681
+ success = False
1931
2682
  continue
1932
2683
  system_name = external_system["external_system_name"]
1933
2684
 
@@ -1937,6 +2688,7 @@ class Payload(object):
1937
2688
  system_name
1938
2689
  )
1939
2690
  )
2691
+ success = False
1940
2692
  continue
1941
2693
  system_type = external_system["external_system_type"]
1942
2694
 
@@ -1954,6 +2706,15 @@ class Payload(object):
1954
2706
  else ""
1955
2707
  )
1956
2708
 
2709
+ # Possible Connection Types for external systems:
2710
+ # "Business Scenario Sample" (Business Scenarios Sample Adapter)
2711
+ # "ot.sap.c4c.SpiAdapter" (SAP C4C SPI Adapter)
2712
+ # "ot.sap.c4c.SpiAdapterV2" (C4C SPI Adapter V2)
2713
+ # "HTTP" (Default WebService Adapter)
2714
+ # "ot.sap.S4HANAAdapter" (S/4HANA SPI Adapter)
2715
+ # "SF" (SalesForce Adapter)
2716
+ # "SFInstance" (SFWebService)
2717
+
1957
2718
  # Set the default settings for the different system types:
1958
2719
  match system_type:
1959
2720
  # Check if we have a SuccessFactors system:
@@ -1993,6 +2754,7 @@ class Payload(object):
1993
2754
  system_name
1994
2755
  )
1995
2756
  )
2757
+ success = False
1996
2758
  continue
1997
2759
  as_url = external_system["as_url"]
1998
2760
 
@@ -2045,6 +2807,7 @@ class Payload(object):
2045
2807
  system_name
2046
2808
  )
2047
2809
  )
2810
+ success = False
2048
2811
  continue
2049
2812
  if not "oauth_client_secret" in external_system:
2050
2813
  logger.error(
@@ -2052,6 +2815,7 @@ class Payload(object):
2052
2815
  system_name
2053
2816
  )
2054
2817
  )
2818
+ success = False
2055
2819
  continue
2056
2820
  oauth_client_id = external_system["oauth_client_id"]
2057
2821
  oauth_client_secret = external_system["oauth_client_secret"]
@@ -2108,6 +2872,7 @@ class Payload(object):
2108
2872
  system_name, connection_type
2109
2873
  )
2110
2874
  )
2875
+ success = False
2111
2876
  else:
2112
2877
  logger.info(
2113
2878
  "Successfully created external system -> {}".format(system_name)
@@ -2127,6 +2892,7 @@ class Payload(object):
2127
2892
  connection_type,
2128
2893
  )
2129
2894
  )
2895
+ success = False
2130
2896
  continue
2131
2897
  saml_url = external_system["saml_url"]
2132
2898
  if not "otds_sp_endpoint" in external_system:
@@ -2136,6 +2902,7 @@ class Payload(object):
2136
2902
  connection_type,
2137
2903
  )
2138
2904
  )
2905
+ success = False
2139
2906
  continue
2140
2907
  otds_sp_endpoint = external_system["otds_sp_endpoint"]
2141
2908
 
@@ -2150,6 +2917,7 @@ class Payload(object):
2150
2917
  logger.info("Successfully added SAML authentication handler.")
2151
2918
  else:
2152
2919
  logger.error("Failed to add SAML authentication handler.")
2920
+ success = False
2153
2921
  case "SAP":
2154
2922
  # Configure a certificate-based SAP authentication handler:
2155
2923
  if not "certificate_file" in external_system:
@@ -2167,6 +2935,7 @@ class Payload(object):
2167
2935
  external_system["certificate_file"],
2168
2936
  )
2169
2937
  )
2938
+ success = False
2170
2939
  continue
2171
2940
  certificate_file = external_system["certificate_file"]
2172
2941
  certificate_password = external_system["certificate_password"]
@@ -2181,6 +2950,7 @@ class Payload(object):
2181
2950
  logger.info("Successfully added SAP authentication handler.")
2182
2951
  else:
2183
2952
  logger.error("Failed to add SAP authentication handler.")
2953
+ success = False
2184
2954
  # Upload and enable certificate file for Archive Center that is required for SAP scenarios
2185
2955
  # we only do this if the necessary information is in payload and if OTAC is enabled:
2186
2956
  if (
@@ -2219,6 +2989,7 @@ class Payload(object):
2219
2989
  connection_type,
2220
2990
  )
2221
2991
  )
2992
+ success = False
2222
2993
  continue
2223
2994
  authorization_endpoint = external_system["authorization_endpoint"]
2224
2995
  if not "token_endpoint" in external_system:
@@ -2228,6 +2999,7 @@ class Payload(object):
2228
2999
  connection_type,
2229
3000
  )
2230
3001
  )
3002
+ success = False
2231
3003
  continue
2232
3004
  token_endpoint = external_system["token_endpoint"]
2233
3005
  response = self._otds.addAuthHandlerOAuth(
@@ -2244,11 +3016,19 @@ class Payload(object):
2244
3016
  if response:
2245
3017
  logger.info("Successfully added OAuth authentication handler.")
2246
3018
  else:
3019
+ success = False
2247
3020
  logger.error("Failed to add OAuth authentication handler.")
2248
3021
 
3022
+ if success:
3023
+ self.writeStatusFile(section_name, self._external_systems)
3024
+
3025
+ return success
3026
+
2249
3027
  # end method definition
2250
3028
 
2251
- def processTransportPackages(self, transport_packages: list):
3029
+ def processTransportPackages(
3030
+ self, transport_packages: list, section_name: str = "transportPackages"
3031
+ ) -> bool:
2252
3032
  """Process transport packages in payload and import them to Extended ECM.
2253
3033
 
2254
3034
  Args:
@@ -2257,14 +3037,28 @@ class Payload(object):
2257
3037
  content_transport, transport_post) so
2258
3038
  we need a parameter
2259
3039
  Returns:
2260
- None
3040
+ bool: True if payload has been processed without errors, False otherwise
2261
3041
  """
2262
3042
 
3043
+ if not transport_packages:
3044
+ logger.info(
3045
+ "Payload section -> {} is empty. Skipping...".format(section_name)
3046
+ )
3047
+ return True
3048
+
3049
+ # if this payload section has been processed successfully before we can return True
3050
+ # and skip processing once more
3051
+ if self.checkStatusFile(section_name):
3052
+ return True
3053
+
3054
+ success: bool = True
3055
+
2263
3056
  for transport_package in transport_packages:
2264
3057
  if not "name" in transport_package:
2265
3058
  logger.error(
2266
3059
  "Transport Package needs a name! Skipping to next transport..."
2267
3060
  )
3061
+ success = False
2268
3062
  continue
2269
3063
  name = transport_package["name"]
2270
3064
 
@@ -2282,6 +3076,7 @@ class Payload(object):
2282
3076
  name
2283
3077
  )
2284
3078
  )
3079
+ success = False
2285
3080
  continue
2286
3081
  if not "description" in transport_package:
2287
3082
  logger.warning(
@@ -2311,22 +3106,41 @@ class Payload(object):
2311
3106
  logger.error(
2312
3107
  "Failed to deploy transport -> {}; URL -> {}".format(name, url)
2313
3108
  )
3109
+ success = False
2314
3110
  if self._stop_on_error:
2315
- return
3111
+ break
2316
3112
  else:
2317
3113
  logger.info("Successfully deployed transport -> {}".format(name))
2318
3114
 
3115
+ if success:
3116
+ self.writeStatusFile(section_name, transport_packages)
3117
+
3118
+ return success
3119
+
2319
3120
  # end method definition
2320
3121
 
2321
- def processUserPhotos(self):
3122
+ def processUserPhotos(self, section_name: str = "userPhotos") -> bool:
2322
3123
  """Process user photos in payload and assign them to Extended ECM users.
2323
3124
 
2324
3125
  Args:
2325
3126
  None
2326
3127
  Returns:
2327
- None
3128
+ bool: True if payload has been processed without errors, False otherwise
2328
3129
  """
2329
3130
 
3131
+ if not self._users:
3132
+ logger.info(
3133
+ "Payload section -> {} is empty. Skipping...".format(section_name)
3134
+ )
3135
+ return True
3136
+
3137
+ # if this payload section has been processed successfully before we can return True
3138
+ # and skip processing once more
3139
+ if self.checkStatusFile(section_name):
3140
+ return True
3141
+
3142
+ success: bool = True
3143
+
2330
3144
  # we assume the nickname of the photo item equals the login name of the user
2331
3145
  # we also assume that the photos have been uploaded / transported into the target system
2332
3146
  for user in self._users:
@@ -2346,6 +3160,7 @@ class Payload(object):
2346
3160
  user_name
2347
3161
  )
2348
3162
  )
3163
+ success = False
2349
3164
  continue
2350
3165
 
2351
3166
  user_id = user["id"]
@@ -2360,8 +3175,9 @@ class Payload(object):
2360
3175
  continue
2361
3176
  photo_id = self._otcs.getResultValue(response, "id")
2362
3177
  response = self._otcs.updateUserPhoto(user_id, photo_id)
2363
- if response == None:
2364
- logger.warning("Failed to add photo for user -> {}".format(user_name))
3178
+ if not response:
3179
+ logger.error("Failed to add photo for user -> {}".format(user_name))
3180
+ success = False
2365
3181
  else:
2366
3182
  logger.info("Successfully added photo for user -> {}".format(user_name))
2367
3183
 
@@ -2369,25 +3185,43 @@ class Payload(object):
2369
3185
  response = self._otcs.getNodeFromNickname("admin")
2370
3186
  if response == None:
2371
3187
  logger.warning("Missing photo for admin - nickname not found. Skipping...")
2372
- return
2373
- photo_id = self._otcs.getResultValue(response, "id")
2374
- response = self._otcs.updateUserPhoto(1000, photo_id)
2375
- if response == None:
2376
- logger.warning("Failed to add photo for admin")
2377
3188
  else:
2378
- logger.info("Successfully added photo for admin")
3189
+ photo_id = self._otcs.getResultValue(response, "id")
3190
+ response = self._otcs.updateUserPhoto(1000, photo_id)
3191
+ if response == None:
3192
+ logger.warning("Failed to add photo for admin")
3193
+ else:
3194
+ logger.info("Successfully added photo for admin")
3195
+
3196
+ if success:
3197
+ self.writeStatusFile(section_name, self._users)
3198
+
3199
+ return success
2379
3200
 
2380
3201
  # end method definition
2381
3202
 
2382
- def processUserPhotosM365(self):
3203
+ def processUserPhotosM365(self, section_name: str = "userPhotosM365") -> bool:
2383
3204
  """Process user photos in payload and assign them to Microsoft 365 users.
2384
3205
 
2385
3206
  Args:
2386
3207
  None
2387
3208
  Returns:
2388
- None
3209
+ bool: True if payload has been processed without errors, False otherwise
2389
3210
  """
2390
3211
 
3212
+ if not self._users:
3213
+ logger.info(
3214
+ "Payload section -> {} is empty. Skipping...".format(section_name)
3215
+ )
3216
+ return True
3217
+
3218
+ # if this payload section has been processed successfully before we can return True
3219
+ # and skip processing once more
3220
+ if self.checkStatusFile(section_name):
3221
+ return True
3222
+
3223
+ success: bool = True
3224
+
2391
3225
  # we assume the nickname of the photo item equals the login name of the user
2392
3226
  # we also assume that the photos have been uploaded / transported into the target system
2393
3227
  for user in self._users:
@@ -2398,6 +3232,7 @@ class Payload(object):
2398
3232
  user_name
2399
3233
  )
2400
3234
  )
3235
+ success = False
2401
3236
  continue
2402
3237
 
2403
3238
  # Check if element has been disabled in payload (enabled = false).
@@ -2420,6 +3255,7 @@ class Payload(object):
2420
3255
  user_name
2421
3256
  )
2422
3257
  )
3258
+ success = False
2423
3259
  continue
2424
3260
 
2425
3261
  user_m365_id = user["m365_id"]
@@ -2456,6 +3292,7 @@ class Payload(object):
2456
3292
  user_name
2457
3293
  )
2458
3294
  )
3295
+ success = False
2459
3296
  else:
2460
3297
  logger.info(
2461
3298
  "Successfully downloaded photo for user -> {} from Extended ECM to file -> {}".format(
@@ -2471,6 +3308,7 @@ class Payload(object):
2471
3308
  user_name
2472
3309
  )
2473
3310
  )
3311
+ success = False
2474
3312
  else:
2475
3313
  logger.info(
2476
3314
  "Successfully uploaded photo for user -> {} to Microsoft 365".format(
@@ -2478,9 +3316,14 @@ class Payload(object):
2478
3316
  )
2479
3317
  )
2480
3318
 
3319
+ if success:
3320
+ self.writeStatusFile(section_name, self._users)
3321
+
3322
+ return success
3323
+
2481
3324
  # end method definition
2482
3325
 
2483
- def processWorkspaceTypes(self) -> list:
3326
+ def processWorkspaceTypes(self, section_name: str = "workspaceTypes") -> list:
2484
3327
  """Create a data structure for all workspace types in the Extended ECM system.
2485
3328
 
2486
3329
  Args:
@@ -2494,6 +3337,11 @@ class Payload(object):
2494
3337
  + id
2495
3338
  """
2496
3339
 
3340
+ # if this payload section has been processed successfully before we can return True
3341
+ # and skip processing once more
3342
+ if self.checkStatusFile(section_name):
3343
+ return True
3344
+
2497
3345
  # get all workspace types (these have been created by the transports and are not in the payload!):
2498
3346
  response = self._otcs.getWorkspaceTypes()
2499
3347
  if response == None:
@@ -2569,113 +3417,36 @@ class Payload(object):
2569
3417
  )
2570
3418
  continue
2571
3419
 
3420
+ self.writeStatusFile(section_name, self._workspace_types)
3421
+
2572
3422
  return self._workspace_types
2573
3423
 
2574
3424
  # end method definition
2575
3425
 
2576
- def processWorkspaceTemplateRegistrations(self):
2577
- """Process workspace template registrations for Extended ECM for Engineering.
2578
- This has been a work-around for a transport issue in 22.4. This code is not required
2579
- for 23.1 or newer. Right now we just disabled the payload. We may consider to remove
2580
- the code in future versions.
2581
-
2582
- Args: None
2583
- Return: None
2584
- """
2585
-
2586
- # loop through the workspace template registrations in the payload:
2587
- for workspace_template_registration in self._workspace_template_registrations:
2588
- # Do some sanity checks first and read the workspace type name
2589
- # and workspace template name from the payload:
2590
- if not "workspace_type_name" in workspace_template_registration:
2591
- logger.error(
2592
- "Workspace template registration needs a workspace type name! Skipping to next workspace template registration..."
2593
- )
2594
- continue
2595
- type_name = workspace_template_registration["workspace_type_name"]
2596
-
2597
- if (
2598
- "enabled" in workspace_template_registration
2599
- and not workspace_template_registration["enabled"]
2600
- ):
2601
- logger.info(
2602
- "Payload for Workspace Template Registration for Workspace Type -> {} is disabled. Skipping...".format(
2603
- type_name
2604
- )
2605
- )
2606
- continue
2607
-
2608
- if not "workspace_template_name" in workspace_template_registration:
2609
- logger.error(
2610
- "Workspace template registration needs a workspace template name! Skipping to next workspace template registration..."
2611
- )
2612
- continue
2613
- template_name = workspace_template_registration["workspace_template_name"]
2614
-
2615
- # now we have the template type name and the template name from the
2616
- # payload. We can now proceed to find these items in the workspace types
2617
- # data structure. First we lookup the workspace type:
2618
- workspace_type = next(
2619
- (item for item in self._workspace_types if item["name"] == type_name),
2620
- None,
2621
- )
2622
- if workspace_type is None:
2623
- logger.error(
2624
- "Workspace Type with name -> {} not found.".format(type_name)
2625
- )
2626
- continue
3426
+ def processWorkspaces(self, section_name: str = "workspaces") -> bool:
3427
+ """Process workspaces in payload and create them in Extended ECM.
2627
3428
 
2628
- # now we use the template list in the workspace type datastructure
2629
- # to find the workspace template:
2630
- workspace_template_list = workspace_type["templates"]
2631
- if workspace_template_list is []:
2632
- logger.error(
2633
- "Workspace Type with name -> {} has no templates.".format(type_name)
2634
- )
2635
- continue
3429
+ Args:
3430
+ None
3431
+ Return:
3432
+ bool: True if payload has been processed without errors, False otherwise
2636
3433
 
2637
- workspace_template = next(
2638
- (
2639
- item
2640
- for item in workspace_template_list
2641
- if item["name"] == template_name
2642
- ),
2643
- None,
2644
- )
2645
- if workspace_template is None:
2646
- logger.error(
2647
- "Workspace Type with name -> {} has no Template with name -> {}.".format(
2648
- type_name, template_name
2649
- )
2650
- )
2651
- continue
2652
- workspace_template_id = workspace_template["id"]
3434
+ Side Effects:
3435
+ Set workspace["nodeId] to the node ID of the created workspace
3436
+ """
2653
3437
 
3438
+ if not self._workspaces:
2654
3439
  logger.info(
2655
- "Register workspace template -> {} ({}) with workspace type -> {}".format(
2656
- template_name, workspace_template_id, type_name
2657
- )
3440
+ "Payload section -> {} is empty. Skipping...".format(section_name)
2658
3441
  )
3442
+ return True
2659
3443
 
2660
- response = self._otcs.registerWorkspaceTemplate(workspace_template_id)
2661
- if response == None:
2662
- logger.error(
2663
- "Failed to register workspace template -> {} with workspace type -> {}".format(
2664
- template_name, type_name
2665
- )
2666
- )
2667
- continue
2668
-
2669
- # end method definition
2670
-
2671
- def processWorkspaces(self):
2672
- """Process workspaces in payload and create them in Extended ECM.
3444
+ # if this payload section has been processed successfully before we can return True
3445
+ # and skip processing once more
3446
+ if self.checkStatusFile(section_name):
3447
+ return True
2673
3448
 
2674
- Args: None
2675
- Return: None
2676
- Side Effects:
2677
- Set workspace["nodeId] to the node ID of the created workspace
2678
- """
3449
+ success: bool = True
2679
3450
 
2680
3451
  for workspace in self._workspaces:
2681
3452
  # Read name from payload:
@@ -2708,14 +3479,10 @@ class Payload(object):
2708
3479
  name, type_name
2709
3480
  )
2710
3481
  )
2711
- response = self._otcs.getWorkspaceByTypeAndName(type_name, name)
2712
- logger.debug(
2713
- "Search for existing workspace delivered -> {}".format(response)
2714
- )
2715
- workspace_id = self._otcs.getResultValue(response, "id")
3482
+ workspace_id = self.determineWorkspaceID(workspace)
2716
3483
  if workspace_id:
2717
3484
  # we still want to set the nodeId as other parts of the payload depend on it:
2718
- workspace["nodeId"] = workspace_id
3485
+ # workspace["nodeId"] = workspace_id
2719
3486
  logger.info(
2720
3487
  "Workspace -> {} of type -> {} does already exist and has ID -> {}! Skipping to next workspace...".format(
2721
3488
  name, type_name, workspace_id
@@ -2752,13 +3519,13 @@ class Payload(object):
2752
3519
  )
2753
3520
  continue
2754
3521
 
2755
- if not "nodeId" in parent_workspace:
3522
+ parent_workspace_node_id = self.determineWorkspaceID(parent_workspace)
3523
+ if not parent_workspace_node_id:
2756
3524
  logger.warning(
2757
3525
  "Parent Workspace without node ID (parent workspace creation may have failed) - skipping to next workspace..."
2758
3526
  )
2759
3527
  continue
2760
- # now determine the actual node IDs of the parent workspace (should have been created before):
2761
- parent_workspace_node_id = parent_workspace["nodeId"]
3528
+
2762
3529
  logger.info(
2763
3530
  "Parent Workspace with logical ID -> {} has node ID -> {}".format(
2764
3531
  parent_id, parent_workspace_node_id
@@ -3347,7 +4114,7 @@ class Payload(object):
3347
4114
  workspace["nodeId"] = self._otcs.getResultValue(response, "id")
3348
4115
 
3349
4116
  # We also get the name the workspace was finally created with.
3350
- # This can be different form the namein the payload as additional
4117
+ # This can be different form the name in the payload as additional
3351
4118
  # naming conventions from the Workspace Type definitions may apply.
3352
4119
  # This is important to make the python container idem-potent.
3353
4120
  response = self._otcs.getWorkspace(workspace["nodeId"])
@@ -3415,9 +4182,16 @@ class Payload(object):
3415
4182
  )
3416
4183
  )
3417
4184
 
4185
+ if success:
4186
+ self.writeStatusFile(section_name, self._workspaces)
4187
+
4188
+ return success
4189
+
3418
4190
  # end method definition
3419
4191
 
3420
- def processWorkspaceRelationships(self):
4192
+ def processWorkspaceRelationships(
4193
+ self, section_name: str = "workspaceRelationships"
4194
+ ) -> bool:
3421
4195
  """Process workspaces relationships in payload and create them in Extended ECM.
3422
4196
 
3423
4197
  Relationships can only be created if all workspaces have been created before.
@@ -3426,10 +4200,25 @@ class Payload(object):
3426
4200
  Relationships are created between the node IDs of two business workspaces
3427
4201
  (and not the logical IDs in the inital payload specification)
3428
4202
 
3429
- Args: None
3430
- Return: None
4203
+ Args:
4204
+ None
4205
+ Return:
4206
+ bool: True if payload has been processed without errors, False otherwise
3431
4207
  """
3432
4208
 
4209
+ if not self._workspaces:
4210
+ logger.info(
4211
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4212
+ )
4213
+ return True
4214
+
4215
+ # if this payload section has been processed successfully before we can return True
4216
+ # and skip processing once more
4217
+ if self.checkStatusFile(section_name):
4218
+ return True
4219
+
4220
+ success: bool = True
4221
+
3433
4222
  for workspace in self._workspaces:
3434
4223
  # Read name from payload:
3435
4224
  if not "name" in workspace:
@@ -3464,13 +4253,13 @@ class Payload(object):
3464
4253
  workspace_id = workspace["id"]
3465
4254
  logger.info("Workspace -> {} has relationships - creating...".format(name))
3466
4255
 
3467
- if not "nodeId" in workspace:
4256
+ workspace_node_id = self.determineWorkspaceID(workspace)
4257
+ if not workspace_node_id:
3468
4258
  logger.warning(
3469
4259
  "Workspace without node ID cannot have a relationship (workspace creation may have failed) - skipping to next workspace..."
3470
4260
  )
3471
4261
  continue
3472
4262
  # now determine the actual node IDs of the workspaces (have been created above):
3473
- workspace_node_id = workspace["nodeId"]
3474
4263
  logger.info(
3475
4264
  "Workspace with logical ID -> {} has node ID -> {}".format(
3476
4265
  workspace_id, workspace_node_id
@@ -3493,6 +4282,7 @@ class Payload(object):
3493
4282
  related_workspace_id
3494
4283
  )
3495
4284
  )
4285
+ success = False
3496
4286
  continue
3497
4287
 
3498
4288
  if "enabled" in related_workspace and not related_workspace["enabled"]:
@@ -3503,13 +4293,13 @@ class Payload(object):
3503
4293
  )
3504
4294
  continue
3505
4295
 
3506
- if not "nodeId" in related_workspace:
4296
+ related_workspace_node_id = self.determineWorkspaceID(related_workspace)
4297
+ if not related_workspace_node_id:
3507
4298
  logger.warning(
3508
4299
  "Related Workspace without node ID (workspaces creation may have failed) - skipping to next workspace..."
3509
4300
  )
3510
4301
  continue
3511
- # now determine the actual node IDs of the related workspace (should have been created above):
3512
- related_workspace_node_id = related_workspace["nodeId"]
4302
+
3513
4303
  logger.info(
3514
4304
  "Related Workspace with logical ID -> {} has node ID -> {}".format(
3515
4305
  related_workspace_id, related_workspace_node_id
@@ -3539,20 +4329,41 @@ class Payload(object):
3539
4329
  response = self._otcs.createWorkspaceRelationship(
3540
4330
  workspace_node_id, related_workspace_node_id
3541
4331
  )
3542
- if response == None:
4332
+ if not response:
3543
4333
  logger.error("Failed to create workspace relationship.")
4334
+ success = False
3544
4335
  else:
3545
4336
  logger.info("Successfully created workspace relationship.")
3546
4337
 
4338
+ if success:
4339
+ self.writeStatusFile(section_name, self._workspaces)
4340
+
4341
+ return success
4342
+
3547
4343
  # end method definition
3548
4344
 
3549
- def processWorkspaceMembers(self):
4345
+ def processWorkspaceMembers(self, section_name: str = "workspaceMembers") -> bool:
3550
4346
  """Process workspaces members in payload and create them in Extended ECM.
3551
4347
 
3552
- Args: None
3553
- Return: None
4348
+ Args:
4349
+ None
4350
+ Return:
4351
+ bool: True if payload has been processed without errors, False otherwise
3554
4352
  """
3555
4353
 
4354
+ if not self._workspaces:
4355
+ logger.info(
4356
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4357
+ )
4358
+ return True
4359
+
4360
+ # if this payload section has been processed successfully before we can return True
4361
+ # and skip processing once more
4362
+ if self.checkStatusFile(section_name):
4363
+ return True
4364
+
4365
+ success: bool = True
4366
+
3556
4367
  for workspace in self._workspaces:
3557
4368
  # Read name from payload (just for logging):
3558
4369
  if not "name" in workspace:
@@ -3586,14 +4397,14 @@ class Payload(object):
3586
4397
  )
3587
4398
  )
3588
4399
 
3589
- if not "nodeId" in workspace:
4400
+ workspace_node_id = self.determineWorkspaceID(workspace)
4401
+ if not workspace_node_id:
3590
4402
  logger.warning(
3591
4403
  "Workspace without node ID cannot have a members (workspaces creation may have failed) - skipping to next workspace..."
3592
4404
  )
3593
4405
  continue
3594
4406
 
3595
4407
  # now determine the actual node IDs of the workspaces (have been created by processWorkspaces()):
3596
- workspace_node_id = workspace["nodeId"]
3597
4408
  workspace_node = self._otcs.getNode(workspace_node_id)
3598
4409
  workspace_owner_id = self._otcs.getResultValue(
3599
4410
  workspace_node, "owner_user_id"
@@ -3656,6 +4467,7 @@ class Payload(object):
3656
4467
  workspace_name
3657
4468
  )
3658
4469
  )
4470
+ success = False
3659
4471
  continue
3660
4472
  if (
3661
4473
  member_users == [] and member_groups == []
@@ -3677,6 +4489,7 @@ class Payload(object):
3677
4489
  workspace_name, member_role_name
3678
4490
  )
3679
4491
  )
4492
+ success = False
3680
4493
  continue
3681
4494
  logger.info("Role -> {} has ID -> {}".format(member_role_name, role_id))
3682
4495
 
@@ -3726,6 +4539,7 @@ class Payload(object):
3726
4539
  workspace_name,
3727
4540
  )
3728
4541
  )
4542
+ success = False
3729
4543
  else:
3730
4544
  logger.info(
3731
4545
  "Successfully added user -> {} ({}) to role -> {} of workspace -> {}".format(
@@ -3746,6 +4560,7 @@ class Payload(object):
3746
4560
  logger.error(
3747
4561
  "Cannot find group with name -> {}".format(member_group)
3748
4562
  )
4563
+ success = False
3749
4564
  continue
3750
4565
  group_id = member_group_id["id"]
3751
4566
 
@@ -3761,6 +4576,7 @@ class Payload(object):
3761
4576
  workspace_name,
3762
4577
  )
3763
4578
  )
4579
+ success = False
3764
4580
  else:
3765
4581
  logger.info(
3766
4582
  "Successfully added group -> {} ({}) to role -> {} of workspace -> {}".format(
@@ -3771,17 +4587,38 @@ class Payload(object):
3771
4587
  )
3772
4588
  )
3773
4589
 
4590
+ if success:
4591
+ self.writeStatusFile(section_name, self._workspaces)
4592
+
4593
+ return success
4594
+
3774
4595
  # end method definition
3775
4596
 
3776
- def processWebReports(self, web_reports: list):
4597
+ def processWebReports(
4598
+ self, web_reports: list, section_name: str = "webReports"
4599
+ ) -> bool:
3777
4600
  """Process web reports in payload and run them in Extended ECM.
3778
4601
 
3779
4602
  Args:
3780
4603
  web_reports (list): list of web reports. As we have two different list (pre and post)
3781
4604
  we need to pass the actual list as parameter.
3782
- Return: None
4605
+ Return:
4606
+ bool: True if payload has been processed without errors, False otherwise
3783
4607
  """
3784
4608
 
4609
+ if not web_reports:
4610
+ logger.info(
4611
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4612
+ )
4613
+ return True
4614
+
4615
+ # if this payload section has been processed successfully before we can return True
4616
+ # and skip processing once more
4617
+ if self.checkStatusFile(section_name):
4618
+ return True
4619
+
4620
+ success: bool = True
4621
+
3785
4622
  for web_report in web_reports:
3786
4623
  nick_name = web_report["nickname"]
3787
4624
 
@@ -3803,6 +4640,7 @@ class Payload(object):
3803
4640
  nick_name
3804
4641
  )
3805
4642
  )
4643
+ success = False
3806
4644
  continue
3807
4645
 
3808
4646
  # be careful to avoid key errors as Web Report parameters are optional:
@@ -3824,6 +4662,7 @@ class Payload(object):
3824
4662
  nick_name
3825
4663
  )
3826
4664
  )
4665
+ success = False
3827
4666
  continue
3828
4667
  lets_continue = False
3829
4668
  # Check 2: Iterate through the actual parameters given in the payload
@@ -3840,6 +4679,7 @@ class Payload(object):
3840
4679
  nick_name, key
3841
4680
  )
3842
4681
  )
4682
+ success = False
3843
4683
  lets_continue = True # we cannot do a "continue" here directly as we are in an inner loop
3844
4684
  # Check 3: Iterate through the formal parameters and validate there's a matching
3845
4685
  # actual parameter defined in the payload for each mandatory formal parameter
@@ -3855,6 +4695,7 @@ class Payload(object):
3855
4695
  nick_name, formal_param["parm_name"]
3856
4696
  )
3857
4697
  )
4698
+ success = False
3858
4699
  lets_continue = True # we cannot do a "continue" here directly as we are in an inner loop
3859
4700
  # Did any of the checks fail?
3860
4701
  if lets_continue:
@@ -3885,6 +4726,7 @@ class Payload(object):
3885
4726
  nick_name, required_param["parm_name"]
3886
4727
  )
3887
4728
  )
4729
+ success = False
3888
4730
  continue
3889
4731
  else: # we are good to proceed!
3890
4732
  logger.debug(
@@ -3895,19 +4737,41 @@ class Payload(object):
3895
4737
  response = self._otcs.runWebReport(nick_name)
3896
4738
  if response == None:
3897
4739
  logger.error("Failed to run web report -> {}".format(nick_name))
4740
+ success = False
4741
+
4742
+ if success:
4743
+ self.writeStatusFile(section_name, web_reports)
4744
+
4745
+ return success
3898
4746
 
3899
4747
  # end method definition
3900
4748
 
3901
- def processCSApplications(self, otcs_object: object = None):
4749
+ def processCSApplications(
4750
+ self, otcs_object: object = None, section_name: str = "csApplications"
4751
+ ) -> bool:
3902
4752
  """Process CS applications in payload and install them in Extended ECM.
3903
4753
  The CS Applications need to be installed in all frontend and backends.
3904
4754
 
3905
4755
  Args:
3906
4756
  otcs_object (object): this can either be the OTCS frontend or OTCS backend. If None
3907
4757
  then the otcs_backend is used.
3908
- Return: None
4758
+ Return:
4759
+ bool: True if payload has been processed without errors, False otherwise
3909
4760
  """
3910
4761
 
4762
+ if not self._cs_applications:
4763
+ logger.info(
4764
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4765
+ )
4766
+ return True
4767
+
4768
+ # if this payload section has been processed successfully before we can return True
4769
+ # and skip processing once more
4770
+ if self.checkStatusFile(section_name):
4771
+ return True
4772
+
4773
+ success: bool = True
4774
+
3911
4775
  # OTCS backend is the default:
3912
4776
  if not otcs_object:
3913
4777
  otcs_object = self._otcs_backend
@@ -3937,19 +4801,38 @@ class Payload(object):
3937
4801
  logger.error(
3938
4802
  "Failed to install CS Application -> {}!".format(application_name)
3939
4803
  )
4804
+ success = False
4805
+
4806
+ if success:
4807
+ self.writeStatusFile(section_name, self._cs_applications)
4808
+
4809
+ return success
3940
4810
 
3941
4811
  # end method definition
3942
4812
 
3943
- def processUserSettings(self):
4813
+ def processUserSettings(self, section_name: str = "userSettings") -> bool:
3944
4814
  """Process user settings in payload and apply themin OTDS.
3945
4815
  This includes password settings and user display settings.
3946
4816
 
3947
4817
  Args:
3948
4818
  None
3949
4819
  Returns:
3950
- None
4820
+ bool: True if payload has been processed without errors, False otherwise
3951
4821
  """
3952
4822
 
4823
+ if not self._users:
4824
+ logger.info(
4825
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4826
+ )
4827
+ return True
4828
+
4829
+ # if this payload section has been processed successfully before we can return True
4830
+ # and skip processing once more
4831
+ if self.checkStatusFile(section_name):
4832
+ return True
4833
+
4834
+ success: bool = True
4835
+
3953
4836
  for user in self._users:
3954
4837
  user_name = user["name"]
3955
4838
 
@@ -3964,6 +4847,8 @@ class Payload(object):
3964
4847
  user_partition = self._otcs.config()["partition"]
3965
4848
  if not user_partition:
3966
4849
  logger.error("User partition not found!")
4850
+ success = False
4851
+ continue
3967
4852
 
3968
4853
  # Set the OTDS display name. Extended ECM does not use this but
3969
4854
  # it makes AppWorks display users correctly (and it doesn't hurt)
@@ -3985,6 +4870,7 @@ class Payload(object):
3985
4870
  user_name, user_display_name
3986
4871
  )
3987
4872
  )
4873
+ success = False
3988
4874
 
3989
4875
  # Don't enforce the user to reset password at first login (settings in OTDS):
3990
4876
  logger.info(
@@ -3993,10 +4879,19 @@ class Payload(object):
3993
4879
  response = self._otds.updateUser(
3994
4880
  user_partition, user_name, "UserMustChangePasswordAtNextSignIn", "False"
3995
4881
  )
4882
+ if not response:
4883
+ success = False
4884
+
4885
+ if success:
4886
+ self.writeStatusFile(section_name, self._users)
4887
+
4888
+ return success
3996
4889
 
3997
4890
  # end method definition
3998
4891
 
3999
- def processUserFavoritesAndProfiles(self):
4892
+ def processUserFavoritesAndProfiles(
4893
+ self, section_name: str = "userFavoritesAndProfiles"
4894
+ ) -> bool:
4000
4895
  """Process user favorites in payload and create them in Extended ECM.
4001
4896
  This method also simulates browsing the favorites to populate the
4002
4897
  widgets on the landing pages and sets personal preferences.
@@ -4004,9 +4899,22 @@ class Payload(object):
4004
4899
  Args:
4005
4900
  None
4006
4901
  Returns:
4007
- None
4902
+ bool: True if payload has been processed without errors, False otherwise
4008
4903
  """
4009
4904
 
4905
+ if not self._users:
4906
+ logger.info(
4907
+ "Payload section -> {} is empty. Skipping...".format(section_name)
4908
+ )
4909
+ return True
4910
+
4911
+ # if this payload section has been processed successfully before we can return True
4912
+ # and skip processing once more
4913
+ if self.checkStatusFile(section_name):
4914
+ return True
4915
+
4916
+ success: bool = True
4917
+
4010
4918
  # We can only set favorites if we impersonate / authenticate as the user.
4011
4919
  # The following code (for loop) will change the authenticated user - we need to
4012
4920
  # switch it back to admin user later so we safe the admin credentials for this:
@@ -4037,6 +4945,7 @@ class Payload(object):
4037
4945
  cookie = self._otcs.authenticate(True)
4038
4946
  if not cookie:
4039
4947
  logger.error("Couldn't authenticate user -> {}".format(user_name))
4948
+ success = False
4040
4949
  continue
4041
4950
 
4042
4951
  # we update the user profile to activate navigation tree:
@@ -4060,34 +4969,21 @@ class Payload(object):
4060
4969
  (item for item in self._workspaces if item["id"] == favorite), None
4061
4970
  )
4062
4971
  is_workspace = False
4063
- if favorite_item is not None:
4972
+ if favorite_item:
4064
4973
  logger.info(
4065
4974
  "Found favorite item (workspace) in payload -> {}".format(
4066
4975
  favorite_item["name"]
4067
4976
  )
4068
4977
  )
4069
- if "nodeId" in favorite_item:
4070
- favorite_id = favorite_item["nodeId"]
4071
- else:
4072
- logger.info(
4073
- "Favorite -> {} (workspace) does not have a nodeId in payload - check if it was created by an external systems...".format(
4074
- favorite
4978
+ favorite_id = self.determineWorkspaceID(favorite_item)
4979
+ if not favorite_id:
4980
+ logger.warning(
4981
+ "Workspace of type -> {} and name -> {} does not exist. Cannot create favorite. Skipping...".format(
4982
+ favorite_item["type_name"], favorite_item["name"]
4075
4983
  )
4076
4984
  )
4077
- response = self._otcs.getWorkspaceByTypeAndName(
4078
- favorite_item["type_name"], favorite_item["name"]
4079
- )
4080
- favorite_id = self._otcs.getResultValue(response, "id")
4081
- if not favorite_id:
4082
- logger.warning(
4083
- "Workspace of type -> {} and name -> {} does not exist. Cannot create favorite. Skipping...".format(
4084
- favorite_item["type_name"], favorite_item["name"]
4085
- )
4086
- )
4087
- continue
4088
- else:
4089
- # store ID in payload in case it is used again
4090
- favorite_item["nodeId"] = favorite_id
4985
+ continue
4986
+
4091
4987
  is_workspace = True
4092
4988
  else:
4093
4989
  # alternatively try to find the item as a nickname:
@@ -4141,6 +5037,7 @@ class Payload(object):
4141
5037
  proxy, user_name
4142
5038
  )
4143
5039
  )
5040
+ success = False
4144
5041
  continue
4145
5042
  proxy_user_id = proxy_user["id"]
4146
5043
 
@@ -4175,15 +5072,37 @@ class Payload(object):
4175
5072
  # True = force new login with new user
4176
5073
  cookie = self._otcs.authenticate(True)
4177
5074
 
5075
+ if success:
5076
+ self.writeStatusFile(section_name, self._users)
5077
+
5078
+ return success
5079
+
4178
5080
  # end method definition
4179
5081
 
4180
- def processSecurityClearances(self):
5082
+ def processSecurityClearances(
5083
+ self, section_name: str = "securityClearances"
5084
+ ) -> bool:
4181
5085
  """Process Security Clearances for Extended ECM.
4182
5086
 
4183
- Args: None
4184
- Return: None
5087
+ Args:
5088
+ None
5089
+ Return:
5090
+ bool: True if payload has been processed without errors, False otherwise
4185
5091
  """
4186
5092
 
5093
+ if not self._security_clearances:
5094
+ logger.info(
5095
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5096
+ )
5097
+ return True
5098
+
5099
+ # if this payload section has been processed successfully before we can return True
5100
+ # and skip processing once more
5101
+ if self.checkStatusFile(section_name):
5102
+ return True
5103
+
5104
+ success: bool = True
5105
+
4187
5106
  for security_clearance in self._security_clearances:
4188
5107
  clearance_level = security_clearance.get("level")
4189
5108
  clearance_name = security_clearance.get("name")
@@ -4212,16 +5131,39 @@ class Payload(object):
4212
5131
  logger.error(
4213
5132
  "Cannot create Security Clearance - either level or name is missing!"
4214
5133
  )
5134
+ success = False
5135
+
5136
+ if success:
5137
+ self.writeStatusFile(section_name, self._security_clearances)
5138
+
5139
+ return success
4215
5140
 
4216
5141
  # end method definition
4217
5142
 
4218
- def processSupplementalMarkings(self):
5143
+ def processSupplementalMarkings(
5144
+ self, section_name: str = "supplementalMarkings"
5145
+ ) -> bool:
4219
5146
  """Process Supplemental Markings for Extended ECM.
4220
5147
 
4221
- Args: None
4222
- Return: None
5148
+ Args:
5149
+ None
5150
+ Return:
5151
+ bool: True if payload has been processed without errors, False otherwise
4223
5152
  """
4224
5153
 
5154
+ if not self._supplemental_markings:
5155
+ logger.info(
5156
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5157
+ )
5158
+ return True
5159
+
5160
+ # if this payload section has been processed successfully before we can return True
5161
+ # and skip processing once more
5162
+ if self.checkStatusFile(section_name):
5163
+ return True
5164
+
5165
+ success: bool = True
5166
+
4225
5167
  for supplemental_marking in self._supplemental_markings:
4226
5168
  code = supplemental_marking.get("code")
4227
5169
 
@@ -4250,16 +5192,37 @@ class Payload(object):
4250
5192
  logger.error(
4251
5193
  "Cannot create Supplemental Marking - either code or description is missing!"
4252
5194
  )
5195
+ success = False
5196
+
5197
+ if success:
5198
+ self.writeStatusFile(section_name, self._supplemental_markings)
5199
+
5200
+ return success
4253
5201
 
4254
5202
  # end method definition
4255
5203
 
4256
- def processUserSecurity(self):
5204
+ def processUserSecurity(self, section_name: str = "userSecurity"):
4257
5205
  """Process Security Clearance and Supplemental Markings for Extended ECM users.
4258
5206
 
4259
- Args: None
4260
- Return: None
5207
+ Args:
5208
+ None
5209
+ Return:
5210
+ bool: True if payload has been processed without errors, False otherwise
4261
5211
  """
4262
5212
 
5213
+ if not self._users:
5214
+ logger.info(
5215
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5216
+ )
5217
+ return True
5218
+
5219
+ # if this payload section has been processed successfully before we can return True
5220
+ # and skip processing once more
5221
+ if self.checkStatusFile(section_name):
5222
+ return True
5223
+
5224
+ success: bool = True
5225
+
4263
5226
  for user in self._users:
4264
5227
  user_id = user.get("id")
4265
5228
  user_name = user.get("name")
@@ -4286,9 +5249,16 @@ class Payload(object):
4286
5249
  user_id, user_supplemental_markings
4287
5250
  )
4288
5251
 
5252
+ if success:
5253
+ self.writeStatusFile(section_name, self._users)
5254
+
5255
+ return success
5256
+
4289
5257
  # end method definition
4290
5258
 
4291
- def processRecordsManagementSettings(self):
5259
+ def processRecordsManagementSettings(
5260
+ self, section_name: str = "recordsManagementSettings"
5261
+ ):
4292
5262
  """Process Records Management Settings for Extended ECM.
4293
5263
  The setting files need to be placed in the OTCS file system file via
4294
5264
  a transport into the Support Asset Volume.
@@ -4297,6 +5267,19 @@ class Payload(object):
4297
5267
  Return: None
4298
5268
  """
4299
5269
 
5270
+ if not self._records_management_settings:
5271
+ logger.info(
5272
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5273
+ )
5274
+ return True
5275
+
5276
+ # if this payload section has been processed successfully before we can return True
5277
+ # and skip processing once more
5278
+ if self.checkStatusFile(section_name):
5279
+ return True
5280
+
5281
+ success: bool = True
5282
+
4300
5283
  if (
4301
5284
  "records_management_system_settings" in self._records_management_settings
4302
5285
  and self._records_management_settings["records_management_system_settings"]
@@ -4308,7 +5291,9 @@ class Payload(object):
4308
5291
  "records_management_system_settings"
4309
5292
  ]
4310
5293
  )
4311
- self._otcs.importRecordsManagementSettings(filename)
5294
+ response = self._otcs.importRecordsManagementSettings(filename)
5295
+ if not response:
5296
+ success = False
4312
5297
 
4313
5298
  if (
4314
5299
  "records_management_codes" in self._records_management_settings
@@ -4318,7 +5303,9 @@ class Payload(object):
4318
5303
  self._custom_settings_dir
4319
5304
  + self._records_management_settings["records_management_codes"]
4320
5305
  )
4321
- self._otcs.importRecordsManagementCodes(filename)
5306
+ response = self._otcs.importRecordsManagementCodes(filename)
5307
+ if not response:
5308
+ success = False
4322
5309
 
4323
5310
  if (
4324
5311
  "records_management_rsis" in self._records_management_settings
@@ -4328,7 +5315,9 @@ class Payload(object):
4328
5315
  self._custom_settings_dir
4329
5316
  + self._records_management_settings["records_management_rsis"]
4330
5317
  )
4331
- self._otcs.importRecordsManagementRSIs(filename)
5318
+ response = self._otcs.importRecordsManagementRSIs(filename)
5319
+ if not response:
5320
+ success = False
4332
5321
 
4333
5322
  if (
4334
5323
  "physical_objects_system_settings" in self._records_management_settings
@@ -4339,7 +5328,9 @@ class Payload(object):
4339
5328
  self._custom_settings_dir
4340
5329
  + self._records_management_settings["physical_objects_system_settings"]
4341
5330
  )
4342
- self._otcs.importPhysicalObjectsSettings(filename)
5331
+ response = self._otcs.importPhysicalObjectsSettings(filename)
5332
+ if not response:
5333
+ success = False
4343
5334
 
4344
5335
  if (
4345
5336
  "physical_objects_codes" in self._records_management_settings
@@ -4349,7 +5340,9 @@ class Payload(object):
4349
5340
  self._custom_settings_dir
4350
5341
  + self._records_management_settings["physical_objects_codes"]
4351
5342
  )
4352
- self._otcs.importPhysicalObjectsCodes(filename)
5343
+ response = self._otcs.importPhysicalObjectsCodes(filename)
5344
+ if not response:
5345
+ success = False
4353
5346
 
4354
5347
  if (
4355
5348
  "physical_objects_locators" in self._records_management_settings
@@ -4359,7 +5352,9 @@ class Payload(object):
4359
5352
  self._custom_settings_dir
4360
5353
  + self._records_management_settings["physical_objects_locators"]
4361
5354
  )
4362
- self._otcs.importPhysicalObjectsLocators(filename)
5355
+ response = self._otcs.importPhysicalObjectsLocators(filename)
5356
+ if not response:
5357
+ success = False
4363
5358
 
4364
5359
  if (
4365
5360
  "security_clearance_codes" in self._records_management_settings
@@ -4369,17 +5364,39 @@ class Payload(object):
4369
5364
  self._custom_settings_dir
4370
5365
  + self._records_management_settings["security_clearance_codes"]
4371
5366
  )
4372
- self._otcs.importSecurityClearanceCodes(filename)
5367
+ response = self._otcs.importSecurityClearanceCodes(filename)
5368
+ if not response:
5369
+ success = False
5370
+
5371
+ if success:
5372
+ self.writeStatusFile(section_name, self._records_management_settings)
5373
+
5374
+ return success
4373
5375
 
4374
5376
  # end method definition
4375
5377
 
4376
- def processHolds(self):
5378
+ def processHolds(self, section_name: str = "holds") -> bool:
4377
5379
  """Process Records Management Holds for Extended ECM users.
4378
5380
 
4379
- Args: None
4380
- Return: None
5381
+ Args:
5382
+ None
5383
+ Return:
5384
+ bool: True if payload has been processed without errors, False otherwise
4381
5385
  """
4382
5386
 
5387
+ if not self._holds:
5388
+ logger.info(
5389
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5390
+ )
5391
+ return True
5392
+
5393
+ # if this payload section has been processed successfully before we can return True
5394
+ # and skip processing once more
5395
+ if self.checkStatusFile(section_name):
5396
+ return True
5397
+
5398
+ success: bool = True
5399
+
4383
5400
  for hold in self._holds:
4384
5401
  if not "name" in hold:
4385
5402
  logger.error("Cannot create Hold without a name! Skipping...")
@@ -4392,6 +5409,7 @@ class Payload(object):
4392
5409
  hold_name
4393
5410
  )
4394
5411
  )
5412
+ success = False
4395
5413
  continue
4396
5414
  hold_type = hold["type"]
4397
5415
 
@@ -4451,16 +5469,40 @@ class Payload(object):
4451
5469
  hold_name, hold["holdID"]
4452
5470
  )
4453
5471
  )
5472
+ else:
5473
+ success = False
5474
+
5475
+ if success:
5476
+ self.writeStatusFile(section_name, self._holds)
5477
+
5478
+ return success
4454
5479
 
4455
5480
  # end method definition
4456
5481
 
4457
- def processAdditionalGroupMembers(self):
5482
+ def processAdditionalGroupMembers(
5483
+ self, section_name: str = "additionalGroupMemberships"
5484
+ ) -> bool:
4458
5485
  """Process additional groups memberships we want to have in OTDS.
4459
5486
 
4460
- Args: None
4461
- Return: None
5487
+ Args:
5488
+ None
5489
+ Return:
5490
+ bool: True if payload has been processed without errors, False otherwise
4462
5491
  """
4463
5492
 
5493
+ if not self._additional_group_members:
5494
+ logger.info(
5495
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5496
+ )
5497
+ return True
5498
+
5499
+ # if this payload section has been processed successfully before we can return True
5500
+ # and skip processing once more
5501
+ if self.checkStatusFile(section_name):
5502
+ return True
5503
+
5504
+ success: bool = True
5505
+
4464
5506
  for additional_group_member in self._additional_group_members:
4465
5507
  if not "parent_group" in additional_group_member:
4466
5508
  logger.error("Missing parent_group! Skipping...")
@@ -4484,6 +5526,7 @@ class Payload(object):
4484
5526
  logger.error(
4485
5527
  "Either group_name or user_name need to be specified! Skipping..."
4486
5528
  )
5529
+ success = False
4487
5530
  continue
4488
5531
  if "group_name" in additional_group_member:
4489
5532
  group_name = additional_group_member["group_name"]
@@ -4499,6 +5542,7 @@ class Payload(object):
4499
5542
  group_name, parent_group
4500
5543
  )
4501
5544
  )
5545
+ success = False
4502
5546
  elif "user_name" in additional_group_member:
4503
5547
  user_name = additional_group_member["user_name"]
4504
5548
  logger.info(
@@ -4513,16 +5557,39 @@ class Payload(object):
4513
5557
  user_name, parent_group
4514
5558
  )
4515
5559
  )
5560
+ success = False
5561
+
5562
+ if success:
5563
+ self.writeStatusFile(section_name, self._additional_group_members)
5564
+
5565
+ return success
4516
5566
 
4517
5567
  # end method definition
4518
5568
 
4519
- def processAdditionalAccessRoleMembers(self):
5569
+ def processAdditionalAccessRoleMembers(
5570
+ self, section_name: str = "additionalAccessRoleMemberships"
5571
+ ) -> bool:
4520
5572
  """Process additional access role memberships we want to have in OTDS.
4521
5573
 
4522
- Args: None
4523
- Return: None
5574
+ Args:
5575
+ None
5576
+ Return:
5577
+ bool: True if payload has been processed without errors, False otherwise
4524
5578
  """
4525
5579
 
5580
+ if not self._additional_access_role_members:
5581
+ logger.info(
5582
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5583
+ )
5584
+ return True
5585
+
5586
+ # if this payload section has been processed successfully before we can return True
5587
+ # and skip processing once more
5588
+ if self.checkStatusFile(section_name):
5589
+ return True
5590
+
5591
+ success: bool = True
5592
+
4526
5593
  for additional_access_role_member in self._additional_access_role_members:
4527
5594
  if not "access_role" in additional_access_role_member:
4528
5595
  logger.error("Missing access_role! Skipping...")
@@ -4548,6 +5615,7 @@ class Payload(object):
4548
5615
  logger.error(
4549
5616
  "Either group_name or user_name need to be specified! Skipping..."
4550
5617
  )
5618
+ success = False
4551
5619
  continue
4552
5620
  if "group_name" in additional_access_role_member:
4553
5621
  group_name = additional_access_role_member["group_name"]
@@ -4563,6 +5631,7 @@ class Payload(object):
4563
5631
  group_name, access_role
4564
5632
  )
4565
5633
  )
5634
+ success = False
4566
5635
  elif "user_name" in additional_access_role_member:
4567
5636
  user_name = additional_access_role_member["user_name"]
4568
5637
  logger.info(
@@ -4577,6 +5646,7 @@ class Payload(object):
4577
5646
  user_name, access_role
4578
5647
  )
4579
5648
  )
5649
+ success = False
4580
5650
  elif "partition_name" in additional_access_role_member:
4581
5651
  partition_name = additional_access_role_member["partition_name"]
4582
5652
  logger.info(
@@ -4593,16 +5663,37 @@ class Payload(object):
4593
5663
  partition_name, access_role
4594
5664
  )
4595
5665
  )
5666
+ success = False
5667
+
5668
+ if success:
5669
+ self.writeStatusFile(section_name, self._additional_access_role_members)
5670
+
5671
+ return success
4596
5672
 
4597
5673
  # end method definition
4598
5674
 
4599
- def processRenamings(self):
5675
+ def processRenamings(self, section_name: str = "renamings") -> bool:
4600
5676
  """Process renamings specified in payload and rename existing Extended ECM items.
4601
5677
 
4602
- Args: None
4603
- Return: None
5678
+ Args:
5679
+ None
5680
+ Return:
5681
+ bool: True if payload has been processed without errors, False otherwise
4604
5682
  """
4605
5683
 
5684
+ if not self._renamings:
5685
+ logger.info(
5686
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5687
+ )
5688
+ return True
5689
+
5690
+ # if this payload section has been processed successfully before we can return True
5691
+ # and skip processing once more
5692
+ if self.checkStatusFile(section_name):
5693
+ return True
5694
+
5695
+ success: bool = True
5696
+
4606
5697
  for renaming in self._renamings:
4607
5698
  if not "nodeid" in renaming:
4608
5699
  if not "volume" in renaming:
@@ -4631,19 +5722,38 @@ class Payload(object):
4631
5722
  node_id, renaming["name"]
4632
5723
  )
4633
5724
  )
5725
+ success = False
5726
+
5727
+ if success:
5728
+ self.writeStatusFile(section_name, self._renamings)
5729
+
5730
+ return success
4634
5731
 
4635
5732
  # end method definition
4636
5733
 
4637
- def processItems(self, items: list):
5734
+ def processItems(self, items: list, section_name: str = "items") -> bool:
4638
5735
  """Process items specified in payload and create them in Extended ECM.
4639
5736
 
4640
5737
  Args:
4641
- otcs: OTCS object
4642
5738
  items: list of items to create (need this as parameter as we
4643
5739
  have multiple lists)
4644
- Return: None
5740
+ Return:
5741
+ bool: True if payload has been processed without errors, False otherwise
4645
5742
  """
4646
5743
 
5744
+ if not items:
5745
+ logger.info(
5746
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5747
+ )
5748
+ return True
5749
+
5750
+ # if this payload section has been processed successfully before we can return True
5751
+ # and skip processing once more
5752
+ if self.checkStatusFile(section_name):
5753
+ return True
5754
+
5755
+ success: bool = True
5756
+
4647
5757
  for item in items:
4648
5758
  if not "name" in item:
4649
5759
  logger.error("Item needs a name. Skipping...")
@@ -4676,6 +5786,7 @@ class Payload(object):
4676
5786
  item_name, parent_nickname
4677
5787
  )
4678
5788
  )
5789
+ success = False
4679
5790
  continue
4680
5791
  else: # use parent_path and Enterprise Volume
4681
5792
  parent_node = self._otcs.getNodeByVolumeAndPath(141, parent_path)
@@ -4687,6 +5798,7 @@ class Payload(object):
4687
5798
  item_name
4688
5799
  )
4689
5800
  )
5801
+ success = False
4690
5802
  continue
4691
5803
 
4692
5804
  original_nickname = item.get("original_nickname")
@@ -4702,6 +5814,7 @@ class Payload(object):
4702
5814
  item_name, original_nickname
4703
5815
  )
4704
5816
  )
5817
+ success = False
4705
5818
  continue
4706
5819
  elif original_path:
4707
5820
  original_node = self._otcs.getNodeByVolumeAndPath(141, original_path)
@@ -4713,12 +5826,14 @@ class Payload(object):
4713
5826
  item_name
4714
5827
  )
4715
5828
  )
5829
+ success = False
4716
5830
  continue
4717
5831
  else:
4718
5832
  original_id = 0
4719
5833
 
4720
5834
  if not "type" in item:
4721
5835
  logger.error("Item -> {} needs a type. Skipping...".format(item_name))
5836
+ success = False
4722
5837
  continue
4723
5838
 
4724
5839
  item_type = item.get("type")
@@ -4734,6 +5849,7 @@ class Payload(object):
4734
5849
  item_name
4735
5850
  )
4736
5851
  )
5852
+ success = False
4737
5853
  continue
4738
5854
  case 1: # Shortcut
4739
5855
  if original_id == 0:
@@ -4742,12 +5858,15 @@ class Payload(object):
4742
5858
  item_name
4743
5859
  )
4744
5860
  )
5861
+ success = False
4745
5862
  continue
4746
5863
 
4747
5864
  # Check if an item with the same name does already exist.
4748
5865
  # This can also be the case if the python container runs a 2nd time.
4749
5866
  # For this reason we are also not issuing an error but just an info (False):
4750
- response = self._otcs.getNodeByParentAndName(parent_id, item_name, False)
5867
+ response = self._otcs.getNodeByParentAndName(
5868
+ parent_id, item_name, show_error=False
5869
+ )
4751
5870
  if self._otcs.getResultValue(response, "name") == item_name:
4752
5871
  logger.info(
4753
5872
  "Item with name -> {} does already exist in parent folder with ID -> {}".format(
@@ -4760,10 +5879,18 @@ class Payload(object):
4760
5879
  )
4761
5880
  if not response:
4762
5881
  logger.error("Failed to create item -> {}.".format(item_name))
5882
+ success = False
5883
+
5884
+ if success:
5885
+ self.writeStatusFile(section_name, items)
5886
+
5887
+ return success
4763
5888
 
4764
5889
  # end method definition
4765
5890
 
4766
- def processPermissions(self, permissions: list):
5891
+ def processPermissions(
5892
+ self, permissions: list, section_name: str = "permissions"
5893
+ ) -> bool:
4767
5894
  """Process items specified in payload and upadate permissions.
4768
5895
 
4769
5896
  Args:
@@ -4791,9 +5918,23 @@ class Payload(object):
4791
5918
  apply_to = 2
4792
5919
  }
4793
5920
 
4794
- Return: None
5921
+ Return:
5922
+ bool: True if payload has been processed without errors, False otherwise
4795
5923
  """
4796
5924
 
5925
+ if not permissions:
5926
+ logger.info(
5927
+ "Payload section -> {} is empty. Skipping...".format(section_name)
5928
+ )
5929
+ return True
5930
+
5931
+ # if this payload section has been processed successfully before we can return True
5932
+ # and skip processing once more
5933
+ if self.checkStatusFile(section_name):
5934
+ return True
5935
+
5936
+ success: bool = True
5937
+
4797
5938
  for permission in permissions:
4798
5939
  if (
4799
5940
  not "path" in permission
@@ -4801,6 +5942,7 @@ class Payload(object):
4801
5942
  and not "nickname" in permission
4802
5943
  ):
4803
5944
  logger.error("Item to change permission is not specified. Skipping...")
5945
+ success = False
4804
5946
  continue
4805
5947
 
4806
5948
  # Check if element has been disabled in payload (enabled = false).
@@ -4827,6 +5969,7 @@ class Payload(object):
4827
5969
  volume_type
4828
5970
  )
4829
5971
  )
5972
+ success = False
4830
5973
  continue
4831
5974
 
4832
5975
  # Check if "path" is in payload and not empty list
@@ -4842,6 +5985,7 @@ class Payload(object):
4842
5985
  node_id = self._otcs.getResultValue(node, "id")
4843
5986
  if not node_id:
4844
5987
  logger.error("Path -> {} does not exist. Skipping...".format(path))
5988
+ success = False
4845
5989
  continue
4846
5990
 
4847
5991
  # Check if "nickname" is in payload and not empty string:
@@ -4856,11 +6000,13 @@ class Payload(object):
4856
6000
  node_id = self._otcs.getResultValue(node, "id")
4857
6001
  if not node_id:
4858
6002
  logger.error("Nickname -> {} does not exist. Skipping...")
6003
+ success = False
4859
6004
  continue
4860
6005
 
4861
6006
  # Now we should have a value for node_id:
4862
6007
  if not node_id:
4863
6008
  logger.error("No node ID found! Skipping permission...")
6009
+ success = False
4864
6010
  continue
4865
6011
  else:
4866
6012
  node_name = self._otcs.getResultValue(node, "name")
@@ -4892,6 +6038,7 @@ class Payload(object):
4892
6038
  node_id
4893
6039
  )
4894
6040
  )
6041
+ success = False
4895
6042
 
4896
6043
  # 2. Process Owner Group Permissions
4897
6044
  if "owner_group_permissions" in permission:
@@ -4910,6 +6057,7 @@ class Payload(object):
4910
6057
  node_id
4911
6058
  )
4912
6059
  )
6060
+ success = False
4913
6061
 
4914
6062
  # 3. Process Public Permissions
4915
6063
  if "public_permissions" in permission:
@@ -4928,6 +6076,7 @@ class Payload(object):
4928
6076
  node_id
4929
6077
  )
4930
6078
  )
6079
+ success = False
4931
6080
  continue
4932
6081
 
4933
6082
  # 3. Process Assigned User Permissions (if specified and not empty)
@@ -4938,6 +6087,7 @@ class Payload(object):
4938
6087
  logger.error(
4939
6088
  "Missing user name or permissions in user permission specificiation. Cannot set user permissions. Skipping..."
4940
6089
  )
6090
+ success = False
4941
6091
  continue
4942
6092
  user_name = user["name"]
4943
6093
  permissions = user["permissions"]
@@ -4948,6 +6098,7 @@ class Payload(object):
4948
6098
  user_name
4949
6099
  )
4950
6100
  )
6101
+ success = False
4951
6102
  continue
4952
6103
  user_id = otcs_user["data"][0]["id"]
4953
6104
  logger.info(
@@ -4964,6 +6115,7 @@ class Payload(object):
4964
6115
  node_id
4965
6116
  )
4966
6117
  )
6118
+ success = False
4967
6119
 
4968
6120
  # 4. Process Assigned Group Permissions (if specified and not empty)
4969
6121
  if "groups" in permission and permission["groups"]:
@@ -4988,6 +6140,7 @@ class Payload(object):
4988
6140
  group_name
4989
6141
  )
4990
6142
  )
6143
+ success = False
4991
6144
  continue
4992
6145
  group_id = otcs_group["data"][0]["id"]
4993
6146
  response = self._otcs.assignPermission(
@@ -4999,21 +6152,43 @@ class Payload(object):
4999
6152
  node_id
5000
6153
  )
5001
6154
  )
6155
+ success = False
6156
+
6157
+ if success:
6158
+ self.writeStatusFile(section_name, permissions)
6159
+
6160
+ return success
5002
6161
 
5003
6162
  # end method definition
5004
6163
 
5005
- def processAssignments(self):
6164
+ def processAssignments(self, section_name: str = "assignments") -> bool:
5006
6165
  """Process assignments specified in payload and assign items (such as workspaces and
5007
6166
  items withnicknames) to users or groups.
5008
6167
 
5009
- Args: None
5010
- Return: None
6168
+ Args:
6169
+ None
6170
+ Return:
6171
+ bool: True if payload has been processed without errors, False otherwise
5011
6172
  """
5012
6173
 
6174
+ if not self._assignments:
6175
+ logger.info(
6176
+ "Payload section -> {} is empty. Skipping...".format(section_name)
6177
+ )
6178
+ return True
6179
+
6180
+ # if this payload section has been processed successfully before we can return True
6181
+ # and skip processing once more
6182
+ if self.checkStatusFile(section_name):
6183
+ return True
6184
+
6185
+ success: bool = True
6186
+
5013
6187
  for assignment in self._assignments:
5014
6188
  # Sanity check: we need a subject - it's mandatory:
5015
6189
  if not "subject" in assignment:
5016
6190
  logger.error("Assignment needs a subject! Skipping assignment...")
6191
+ success = False
5017
6192
  continue
5018
6193
  subject = assignment["subject"]
5019
6194
 
@@ -5042,6 +6217,7 @@ class Payload(object):
5042
6217
  subject
5043
6218
  )
5044
6219
  )
6220
+ success = False
5045
6221
  continue
5046
6222
  # Check if a workspace is specified for the assignment and check it does exist:
5047
6223
  if "workspace" in assignment and assignment["workspace"]:
@@ -5053,14 +6229,23 @@ class Payload(object):
5053
6229
  ),
5054
6230
  None,
5055
6231
  )
5056
- if not workspace or not "nodeId" in workspace:
6232
+ if not workspace:
6233
+ logger.error(
6234
+ "Assignment -> {} has specified a not existing workspace -> {}! Skipping assignment...".format(
6235
+ subject, assignment["workspace"]
6236
+ )
6237
+ )
6238
+ success = False
6239
+ continue
6240
+ node_id = self.determineWorkspaceID(workspace)
6241
+ if not node_id:
5057
6242
  logger.error(
5058
6243
  "Assignment -> {} has specified a not existing workspace -> {}! Skipping assignment...".format(
5059
6244
  subject, assignment["workspace"]
5060
6245
  )
5061
6246
  )
6247
+ success = False
5062
6248
  continue
5063
- node_id = workspace["nodeId"]
5064
6249
  # If we don't have a workspace then check if a nickname is specified for the assignment:
5065
6250
  elif "nickname" in assignment:
5066
6251
  response = self._otcs.getNodeFromNickname(assignment["nickname"])
@@ -5072,6 +6257,7 @@ class Payload(object):
5072
6257
  assignment["nickname"]
5073
6258
  )
5074
6259
  )
6260
+ success = False
5075
6261
  continue
5076
6262
  else:
5077
6263
  logger.error(
@@ -5079,6 +6265,7 @@ class Payload(object):
5079
6265
  subject
5080
6266
  )
5081
6267
  )
6268
+ success = False
5082
6269
  continue
5083
6270
 
5084
6271
  assignees = []
@@ -5101,6 +6288,7 @@ class Payload(object):
5101
6288
  group_assignee
5102
6289
  )
5103
6290
  )
6291
+ success = False
5104
6292
  continue
5105
6293
  if not "id" in group:
5106
6294
  logger.error(
@@ -5108,6 +6296,7 @@ class Payload(object):
5108
6296
  group_assignee
5109
6297
  )
5110
6298
  )
6299
+ success = False
5111
6300
  continue
5112
6301
  group_id = group["id"]
5113
6302
  # add the group ID to the assignee list:
@@ -5127,6 +6316,7 @@ class Payload(object):
5127
6316
  user_assignee
5128
6317
  )
5129
6318
  )
6319
+ success = False
5130
6320
  continue
5131
6321
  if not "id" in user:
5132
6322
  logger.error(
@@ -5134,6 +6324,7 @@ class Payload(object):
5134
6324
  user_assignee
5135
6325
  )
5136
6326
  )
6327
+ success = False
5137
6328
  continue
5138
6329
  user_id = user["id"]
5139
6330
  # add the group ID to the assignee list:
@@ -5145,7 +6336,8 @@ class Payload(object):
5145
6336
  subject, node_id
5146
6337
  )
5147
6338
  )
5148
- return
6339
+ success = False
6340
+ continue
5149
6341
 
5150
6342
  response = self._otcs.assignItemToUserGroup(
5151
6343
  node_id, subject, instruction, assignees
@@ -5156,6 +6348,12 @@ class Payload(object):
5156
6348
  subject, node_id, assignees
5157
6349
  )
5158
6350
  )
6351
+ success = False
6352
+
6353
+ if success:
6354
+ self.writeStatusFile(section_name, self._assignments)
6355
+
6356
+ return success
5159
6357
 
5160
6358
  # end method definition
5161
6359
 
@@ -5165,7 +6363,8 @@ class Payload(object):
5165
6363
  license_feature: str,
5166
6364
  license_name: str,
5167
6365
  user_specific_payload_field: str = "licenses",
5168
- ):
6366
+ section_name: str = "userLicenses",
6367
+ ) -> bool:
5169
6368
  """Assign a specific OTDS license feature to all Extended ECM users.
5170
6369
  This method is used for OTIV and Extended ECM licenses.
5171
6370
 
@@ -5175,20 +6374,34 @@ class Payload(object):
5175
6374
  license_name: Name of the license Key (e.g. "EXTENDED_ECM" or "INTELLIGENT_VIEWING")
5176
6375
  user_specific_payload_field: name of the user specific field in payload
5177
6376
  (if empty it will be ignored)
5178
- Return: None
6377
+ Return:
6378
+ bool: True if payload has been processed without errors, False otherwise
5179
6379
  """
5180
6380
 
6381
+ if not self._users:
6382
+ logger.info(
6383
+ "Payload section -> {} is empty. Skipping...".format(section_name)
6384
+ )
6385
+ return True
6386
+
6387
+ # if this payload section has been processed successfully before we can return True
6388
+ # and skip processing once more
6389
+ if self.checkStatusFile(section_name):
6390
+ return True
6391
+
6392
+ success: bool = True
6393
+
5181
6394
  otds_resource = self._otds.getResource(resource_name)
5182
6395
  if not otds_resource:
5183
6396
  logger.error(
5184
6397
  "OTDS Resource -> {} not found. Cannot assign licenses to users."
5185
6398
  )
5186
- return
6399
+ return False
5187
6400
 
5188
6401
  user_partition = self._otcs.config()["partition"]
5189
6402
  if not user_partition:
5190
6403
  logger.error("OTCS user partition not found in OTDS!")
5191
- return
6404
+ return False
5192
6405
 
5193
6406
  for user in self._users:
5194
6407
  user_name = user["name"]
@@ -5211,36 +6424,70 @@ class Payload(object):
5211
6424
  else: # use the default feature from the actual parameter
5212
6425
  user_license_feature = [license_feature]
5213
6426
 
5214
- for lic_feature in user_license_feature:
6427
+ for license_feature in user_license_feature:
6428
+ if self._otds.isUserLicensed(
6429
+ user_name=user_name,
6430
+ resource_id=otds_resource["resourceID"],
6431
+ license_feature=license_feature,
6432
+ license_name=license_name,
6433
+ ):
6434
+ logger.info(
6435
+ "User -> {} is already licensed for -> {} ({})".format(
6436
+ user_name, license_name, license_feature
6437
+ )
6438
+ )
6439
+ continue
5215
6440
  assigned_license = self._otds.assignUserToLicense(
5216
6441
  user_partition,
5217
6442
  user_name, # we want the plain login name here
5218
6443
  otds_resource["resourceID"],
5219
- lic_feature,
6444
+ license_feature,
5220
6445
  license_name,
5221
6446
  )
5222
6447
 
5223
6448
  if not assigned_license:
5224
6449
  logger.error(
5225
6450
  "Failed to assign license feature -> {} to user -> {}!".format(
5226
- lic_feature, user["name"]
6451
+ license_feature, user_name
5227
6452
  )
5228
6453
  )
6454
+ success = False
6455
+
6456
+ if success:
6457
+ self.writeStatusFile(section_name, self._users)
6458
+
6459
+ return success
5229
6460
 
5230
6461
  # end method definition
5231
6462
 
5232
- def processExecPodCommands(self):
6463
+ def processExecPodCommands(self, section_name: str = "execPodCommands") -> bool:
5233
6464
  """Process commands that should be executed in the Kubernetes pods.
5234
6465
 
5235
- Args: None
5236
- Return: None
6466
+ Args:
6467
+ None
6468
+ Return:
6469
+ None
5237
6470
  """
5238
6471
 
6472
+ if not self._exec_pod_commands:
6473
+ logger.info(
6474
+ "Payload section -> {} is empty. Skipping...".format(section_name)
6475
+ )
6476
+ return True
6477
+
6478
+ # if this payload section has been processed successfully before we can return True
6479
+ # and skip processing once more
6480
+ if self.checkStatusFile(section_name):
6481
+ return True
6482
+
6483
+ success: bool = True
6484
+
5239
6485
  for exec_pod_command in self._exec_pod_commands:
5240
6486
  if not "pod_name" in exec_pod_command:
5241
6487
  logger.error(
5242
6488
  "To execute a command in a pod the pod name needs to be specified in the payload! Skipping to next pod command..."
5243
6489
  )
6490
+ success = False
5244
6491
  continue
5245
6492
  pod_name = exec_pod_command["pod_name"]
5246
6493
 
@@ -5250,6 +6497,7 @@ class Payload(object):
5250
6497
  pod_name
5251
6498
  )
5252
6499
  )
6500
+ success = False
5253
6501
  continue
5254
6502
  command = exec_pod_command["command"]
5255
6503
 
@@ -5304,17 +6552,37 @@ class Payload(object):
5304
6552
  )
5305
6553
  )
5306
6554
 
6555
+ if success:
6556
+ self.writeStatusFile(section_name, self._exec_pod_commands)
6557
+
6558
+ return success
6559
+
5307
6560
  # end method definition
5308
6561
 
5309
- def processDocumentGenerators(self):
6562
+ def processDocumentGenerators(
6563
+ self, section_name: str = "documentGenerators"
6564
+ ) -> bool:
5310
6565
  """Generate documents for a defined workspace type based on template
5311
6566
 
5312
6567
  Args:
5313
- none
6568
+ None
5314
6569
  Returns:
5315
- none
6570
+ bool: True if payload has been processed without errors, False otherwise
5316
6571
  """
5317
6572
 
6573
+ if not self._doc_generators:
6574
+ logger.info(
6575
+ "Payload section -> {} is empty. Skipping...".format(section_name)
6576
+ )
6577
+ return True
6578
+
6579
+ # if this payload section has been processed successfully before we can return True
6580
+ # and skip processing once more
6581
+ if self.checkStatusFile(section_name):
6582
+ return True
6583
+
6584
+ success: bool = True
6585
+
5318
6586
  # save admin credentials for later switch back to admin user:
5319
6587
  admin_credentials = self._otcs.credentials()
5320
6588
  authenticated_user = "admin"
@@ -5324,6 +6592,7 @@ class Payload(object):
5324
6592
  logger.error(
5325
6593
  "To generate documents for workspaces the workspace type needs to be specified in the payload! Skipping to next document generator..."
5326
6594
  )
6595
+ success = False
5327
6596
  continue
5328
6597
  workspace_type = doc_generator["workspace_type"]
5329
6598
 
@@ -5333,6 +6602,7 @@ class Payload(object):
5333
6602
  workspace_type
5334
6603
  )
5335
6604
  )
6605
+ success = False
5336
6606
  continue
5337
6607
  template_path = doc_generator["template_path"]
5338
6608
 
@@ -5352,6 +6622,7 @@ class Payload(object):
5352
6622
  workspace_type
5353
6623
  )
5354
6624
  )
6625
+ success = False
5355
6626
  continue
5356
6627
  classification_path = doc_generator["classification_path"]
5357
6628
 
@@ -5361,6 +6632,7 @@ class Payload(object):
5361
6632
  workspace_type
5362
6633
  )
5363
6634
  )
6635
+ success = False
5364
6636
  continue
5365
6637
  category_name = doc_generator["category_name"]
5366
6638
 
@@ -5370,6 +6642,7 @@ class Payload(object):
5370
6642
  workspace_type
5371
6643
  )
5372
6644
  )
6645
+ success = False
5373
6646
  continue
5374
6647
  attributes = doc_generator["attributes"]
5375
6648
 
@@ -5419,6 +6692,7 @@ class Payload(object):
5419
6692
  )
5420
6693
  )
5421
6694
  admin_context = True
6695
+ success = False
5422
6696
  else:
5423
6697
  admin_context = True
5424
6698
 
@@ -5472,6 +6746,7 @@ class Payload(object):
5472
6746
  attribute_value
5473
6747
  )
5474
6748
  )
6749
+ success = False
5475
6750
  continue
5476
6751
  attribute_value = user["data"][0]["id"]
5477
6752
 
@@ -5532,6 +6807,7 @@ class Payload(object):
5532
6807
  document_name, workspace_name
5533
6808
  )
5534
6809
  )
6810
+ success = False
5535
6811
  else:
5536
6812
  logger.info(
5537
6813
  "Successfully generated document -> {} in workspace -> {}".format(
@@ -5554,6 +6830,11 @@ class Payload(object):
5554
6830
  # True = force new login with new user
5555
6831
  cookie = self._otcs.authenticate(True)
5556
6832
 
6833
+ if success:
6834
+ self.writeStatusFile(section_name, self._doc_generators)
6835
+
6836
+ return success
6837
+
5557
6838
  # end method definition
5558
6839
 
5559
6840
  def initSAP(
@@ -5620,14 +6901,28 @@ class Payload(object):
5620
6901
 
5621
6902
  # end method definition
5622
6903
 
5623
- def processSAPRFCs(self, sap_object: object):
6904
+ def processSAPRFCs(self, sap_object: object, section_name: str = "sapRFCs") -> bool:
5624
6905
  """Process SAP RFCs in payload and run them in SAP S/4HANA.
5625
6906
 
5626
6907
  Args:
5627
6908
  sap_object: SAP object
5628
- Return: None
6909
+ Return:
6910
+ bool: True if payload has been processed without errors, False otherwise
5629
6911
  """
5630
6912
 
6913
+ if not self._doc_generators:
6914
+ logger.info(
6915
+ "Payload section -> {} is empty. Skipping...".format(section_name)
6916
+ )
6917
+ return True
6918
+
6919
+ # if this payload section has been processed successfully before we can return True
6920
+ # and skip processing once more
6921
+ if self.checkStatusFile(section_name):
6922
+ return True
6923
+
6924
+ success: bool = True
6925
+
5631
6926
  for sap_rfc in self._sap_rfcs:
5632
6927
  rfc_name = sap_rfc["name"]
5633
6928
 
@@ -5670,6 +6965,7 @@ class Payload(object):
5670
6965
  result = sap_object.call(rfc_name, rfc_call_options, rfc_params)
5671
6966
  if result == None:
5672
6967
  logger.error("Failed to call SAP RFC -> {}".format(rfc_name))
6968
+ success = False
5673
6969
  else:
5674
6970
  logger.info(
5675
6971
  "Successfully called RFC -> {}. Result -> {}".format(
@@ -5677,6 +6973,11 @@ class Payload(object):
5677
6973
  )
5678
6974
  )
5679
6975
 
6976
+ if success:
6977
+ self.writeStatusFile(section_name, self._sap_rfcs)
6978
+
6979
+ return success
6980
+
5680
6981
  # end method definition
5681
6982
 
5682
6983
  def getPayload(self) -> dict: