11
11
import xml .etree .ElementTree as eTree
12
12
from xmljson import badgerfish as bf
13
13
import time
14
+ import math
14
15
15
16
logger = logging .getLogger ('log' )
16
17
logger .setLevel (logging .INFO )
19
20
logger .addHandler (ch )
20
21
CUR_DIR = os .path .dirname (os .path .abspath (__file__ ))
21
22
23
+ RELATIONSHIP_BATCH_SIZE = 20
24
+ # With v1 of the API, we were able to create about 4 relationships per second.
25
+ # So we will assume that we will be able to create them at the same rate with
26
+ # the asynchronous background jobs.
27
+ RELATIONSHIPS_CREATED_PER_SECOND = 4
28
+ # The number of seconds to wait before we check the status of create relationships jobs.
29
+ RELATIONSHIPS_JOB_WAIT_SECONDS = int (math .ceil (RELATIONSHIP_BATCH_SIZE / float (RELATIONSHIPS_CREATED_PER_SECOND )))
30
+ ASSET_TYPE_BUSINESS_SERVICE = "Business Service"
31
+
22
32
parser = argparse .ArgumentParser (description = "freshservice" )
23
33
24
34
parser .add_argument ('-d' , '--debug' , action = 'store_true' , help = 'Enable debug output' )
@@ -225,11 +235,21 @@ def update_objects_from_server(sources, _target, mapping):
225
235
data [map_info ["@target" ]] = value
226
236
227
237
if existing_object is None :
228
- logger .info ("adding device %s" % source ["name" ])
238
+ logger .info ("adding asset %s" % source ["name" ])
229
239
new_asset_id = freshservice .insert_asset (data )
230
240
logger .info ("added new asset %d" % new_asset_id )
231
241
else :
232
- logger .info ("updating device %s" % source ["name" ])
242
+ logger .info ("updating asset %s" % source ["name" ])
243
+ # This is a workaround for an issue with the Freshservice API where if a business service
244
+ # asset has the Managed By field filled in and we don't send an agent_id to update this
245
+ # field (we don't map any D42 data to this field and shouldn't need to because
246
+ # the API will only update the fields that we send), it will result in a validation
247
+ # error with the message:
248
+ # Assigned agent isn't a member of the group.
249
+ # So, if the business service asset has an agent_id already populated, we will send that
250
+ # same value over and that will avoid this error.
251
+ if _target ["@asset-type" ] == ASSET_TYPE_BUSINESS_SERVICE and "agent_id" in existing_object and existing_object ["agent_id" ]:
252
+ data ["agent_id" ] = existing_object ["agent_id" ]
233
253
updated_asset_id = freshservice .update_asset (data , existing_object ["display_id" ])
234
254
logger .info ("updated new asset %d" % updated_asset_id )
235
255
@@ -384,16 +404,20 @@ def create_relationships_from_affinity_group(sources, _target, mapping):
384
404
logger .info ("finished getting all existing devices in FS." )
385
405
386
406
logger .info ("Getting relationship type in FS." )
387
- relationship_type = freshservice .get_relationship_type_by_content (mapping ["@forward -relationship" ],
388
- mapping ["@backward -relationship" ])
407
+ relationship_type = freshservice .get_relationship_type_by_content (mapping ["@downstream -relationship" ],
408
+ mapping ["@upstream -relationship" ])
389
409
logger .info ("finished getting relationship type in FS." )
390
410
if relationship_type is None :
391
411
log = "There is no relationship type in FS. (%s - %s)" % (
392
- mapping ["@forward -relationship" ], mapping ["@backward -relationship" ])
412
+ mapping ["@downstream -relationship" ], mapping ["@upstream -relationship" ])
393
413
logger .info (log )
394
414
return
395
415
396
- for source in sources :
416
+ relationships_to_create = list ()
417
+ source_count = len (sources )
418
+ submitted_jobs = list ()
419
+
420
+ for idx , source in enumerate (sources ):
397
421
try :
398
422
logger .info ("Processing %s - %s." % (source [mapping ["@key" ]], source [mapping ["@target-key" ]]))
399
423
primary_asset = find_object_by_name (existing_objects , source [mapping ["@key" ]])
@@ -413,25 +437,122 @@ def create_relationships_from_affinity_group(sources, _target, mapping):
413
437
exist = False
414
438
for relationship in relationships :
415
439
if relationship ["relationship_type_id" ] == relationship_type ["id" ]:
416
- if relationship ["config_item" ][ "display_id " ] == secondary_asset ["display_id" ]:
440
+ if relationship ["secondary_id " ] == secondary_asset ["display_id" ]:
417
441
exist = True
418
442
break
419
443
if exist :
420
444
logger .info ("There is already relationship in FS." )
421
445
continue
422
446
423
- data = dict ()
424
- data ["type" ] = "config_items"
425
- data ["type_id" ] = [secondary_asset ["display_id" ]]
426
- data ["relationship_type_id" ] = relationship_type ["id" ]
427
- data ["relationship_type" ] = "forward_relationship"
428
- logger .info ("adding relationship %s" % source [mapping ["@key" ]])
429
- new_relationship_id = freshservice .insert_relationship (primary_asset ["display_id" ], data )
430
- logger .info ("added new relationship %d" % new_relationship_id )
447
+ relationships_to_create .append ({
448
+ "relationship_type_id" : relationship_type ["id" ],
449
+ "primary_id" : primary_asset ["display_id" ],
450
+ "primary_type" : "asset" ,
451
+ "secondary_id" : secondary_asset ["display_id" ],
452
+ "secondary_type" : "asset"
453
+ })
454
+
455
+ # Create a new job if we reached our batch size or we are on the last item (which
456
+ # means this is the last batch we will be submitting).
457
+ if len (relationships_to_create ) >= RELATIONSHIP_BATCH_SIZE or idx == source_count - 1 :
458
+ submitted_jobs .append (submit_relationship_create_job (relationships_to_create ))
459
+
460
+ # Clear the list for the next batch of relationships we are going to send.
461
+ del relationships_to_create [:]
431
462
except Exception as e :
432
463
log = "Error (%s) creating relationship %s" % (str (e ), source [mapping ["@key" ]])
433
464
logger .exception (log )
434
465
466
+ # We may not have submitted the last batch of relationships to create if the last item in
467
+ # sources did not result in a relationship needing to be created (e.g. one of the assets
468
+ # in the relationship did not exist in Freshservice, the relationship already existed in
469
+ # Freshservice, etc.). So if we have any relationships that we need to create that have
470
+ # not been submitted, submit them now.
471
+ if relationships_to_create :
472
+ submitted_jobs .append (submit_relationship_create_job (relationships_to_create ))
473
+
474
+ del relationships_to_create [:]
475
+
476
+ if submitted_jobs :
477
+ jobs_to_check = list (submitted_jobs )
478
+ next_jobs_to_check = list ()
479
+
480
+ # We will make attempts to check the status of the jobs and see if they have
481
+ # completed. The max time we will wait is the number of jobs we submitted
482
+ # times the amount of time it takes to create a full batch of relationships.
483
+ # This total wait time will be broken into chunks based on how long it would
484
+ # take a single batch of relationships to be created. For example, if we
485
+ # submitted 3 jobs and each job had a batch of 20 relationships to create,
486
+ # then it should take 5 seconds to create the 20 relationships based on being
487
+ # able to create them at a rate of 4 per second. We will wait 5 seconds, then
488
+ # check the status of all jobs. If there are any jobs still waiting to complete,
489
+ # then we will wait another 5 seconds and check the status of the jobs that were
490
+ # previously waiting to complete.
491
+ # Added 20% padding to wait a little bit longer for the jobs to complete
492
+ # if needed.
493
+ for i in range (int (math .ceil (len (submitted_jobs ) * 1.2 ))):
494
+ time .sleep (RELATIONSHIPS_JOB_WAIT_SECONDS )
495
+
496
+ for job_to_check in jobs_to_check :
497
+ try :
498
+ job = freshservice .get_job (job_to_check ["job_id" ])
499
+ status = job ["status" ]
500
+
501
+ if status == "success" :
502
+ # All relationships were created.
503
+ logger .info ("Job %s created all %d relationships successfully." % (job_to_check ["job_id" ], job_to_check ["relationships_to_create_count" ]))
504
+ elif status in ["failed" , "partial" ]:
505
+ # No relationships were created (failed status) or some relationships
506
+ # were created and some were not (partial status).
507
+ for relationship in job ["relationships" ]:
508
+ if not relationship ["success" ]:
509
+ log = "Job %s failed to create relationship: %s" % (job_to_check ["job_id" ], relationship )
510
+ logger .error (log )
511
+ elif status in ["queued" , "in progress" ]:
512
+ # The job has not completed yet.
513
+ next_jobs_to_check .append (job_to_check )
514
+ log = "Job %s has not completed yet. The job status is %s." % (job_to_check ["job_id" ], status )
515
+ logger .info (log )
516
+ else :
517
+ raise Exception ("Received unknown job status of %s." % status )
518
+ except Exception as e :
519
+ log = "Error (%s) checking job %s" % (str (e ), job_to_check ["job_id" ])
520
+ logger .exception (log )
521
+
522
+ # Clear the list.
523
+ del jobs_to_check [:]
524
+
525
+ if next_jobs_to_check :
526
+ # We still have jobs we need to check.
527
+ jobs_to_check .extend (next_jobs_to_check )
528
+
529
+ # Clear the list so that we can add the next set of jobs that are
530
+ # still waiting to complete.
531
+ del next_jobs_to_check [:]
532
+ else :
533
+ # There are no more jobs that we need to check, so we can stop
534
+ # checking.
535
+ break
536
+
537
+ if jobs_to_check :
538
+ submitted_jobs_count = len (submitted_jobs )
539
+ jobs_not_completed_count = len (jobs_to_check )
540
+
541
+ logger .info ("%d of %d relationship create jobs did not complete." % (jobs_not_completed_count , submitted_jobs_count ))
542
+
543
+
544
+ def submit_relationship_create_job (relationships_to_create ):
545
+ logger .info ("adding relationship create job" )
546
+ # Creating relationships using the v2 API is now an asynchronous operation and is
547
+ # performed using background jobs. We will get back the job ID which can then be
548
+ # used to query the status of the job.
549
+ job_id = freshservice .insert_relationships ({"relationships" : relationships_to_create })
550
+ logger .info ("added new relationship create job %s" % job_id )
551
+
552
+ return {
553
+ "job_id" : job_id ,
554
+ "relationships_to_create_count" : len (relationships_to_create )
555
+ }
435
556
436
557
def delete_relationships_from_affinity_group (sources , _target , mapping ):
437
558
global freshservice
@@ -441,12 +562,12 @@ def delete_relationships_from_affinity_group(sources, _target, mapping):
441
562
logger .info ("finished getting all existing devices in FS." )
442
563
443
564
logger .info ("Getting relationship type in FS." )
444
- relationship_type = freshservice .get_relationship_type_by_content (mapping ["@forward -relationship" ],
445
- mapping ["@backward -relationship" ])
565
+ relationship_type = freshservice .get_relationship_type_by_content (mapping ["@downstream -relationship" ],
566
+ mapping ["@upstream -relationship" ])
446
567
logger .info ("finished getting relationship type in FS." )
447
568
if relationship_type is None :
448
569
log = "There is no relationship type in FS. (%s - %s)" % (
449
- mapping ["@forward -relationship" ], mapping ["@backward -relationship" ])
570
+ mapping ["@downstream -relationship" ], mapping ["@upstream -relationship" ])
450
571
logger .info (log )
451
572
return
452
573
@@ -468,14 +589,14 @@ def delete_relationships_from_affinity_group(sources, _target, mapping):
468
589
remove_relationship = None
469
590
for relationship in relationships :
470
591
if relationship ["relationship_type_id" ] == relationship_type ["id" ]:
471
- if relationship ["config_item" ][ "display_id " ] == secondary_asset ["display_id" ]:
592
+ if relationship ["secondary_id " ] == secondary_asset ["display_id" ]:
472
593
remove_relationship = relationship
473
594
break
474
595
if remove_relationship is None :
475
596
logger .info ("There is no relationship in FS." )
476
597
continue
477
598
478
- freshservice .detach_relationship (primary_asset [ "display_id" ], remove_relationship ["id" ])
599
+ freshservice .detach_relationship (remove_relationship ["id" ])
479
600
logger .info ("detached relationship %d" % remove_relationship ["id" ])
480
601
except Exception as e :
481
602
log = "Error (%s) deleting relationship %s" % (str (e ), source [mapping ["@key" ]])
@@ -494,12 +615,12 @@ def delete_relationships_from_business_app(sources, _target, mapping):
494
615
logger .info ("finished getting all existing devices in FS." )
495
616
496
617
logger .info ("Getting relationship type in FS." )
497
- relationship_type = freshservice .get_relationship_type_by_content (mapping ["@forward -relationship" ],
498
- mapping ["@backward -relationship" ])
618
+ relationship_type = freshservice .get_relationship_type_by_content (mapping ["@downstream -relationship" ],
619
+ mapping ["@upstream -relationship" ])
499
620
logger .info ("finished getting relationship type in FS." )
500
621
if relationship_type is None :
501
622
log = "There is no relationship type in FS. (%s - %s)" % (
502
- mapping ["@forward -relationship" ], mapping ["@backward -relationship" ])
623
+ mapping ["@downstream -relationship" ], mapping ["@upstream -relationship" ])
503
624
logger .info (log )
504
625
return
505
626
@@ -509,9 +630,9 @@ def delete_relationships_from_business_app(sources, _target, mapping):
509
630
relationships = freshservice .get_relationships_by_id (existing_object ["display_id" ])
510
631
for relationship in relationships :
511
632
if relationship ["relationship_type_id" ] == relationship_type ["id" ] and \
512
- relationship ["relationship_type " ] == "forward_relationship" :
633
+ relationship ["primary_id " ] == existing_object [ "display_id" ] :
513
634
remove_relationship = relationship
514
- target_display_id = relationship ["config_item" ][ "display_id " ]
635
+ target_display_id = relationship ["secondary_id " ]
515
636
for source in sources :
516
637
if source [mapping ["@key" ]] == existing_object ["name" ]:
517
638
secondary_asset = find_object_by_name (existing_objects , source [mapping ["@target-key" ]])
@@ -522,7 +643,7 @@ def delete_relationships_from_business_app(sources, _target, mapping):
522
643
if remove_relationship is None :
523
644
continue
524
645
525
- freshservice .detach_relationship (existing_object [ "display_id" ], remove_relationship ["id" ])
646
+ freshservice .detach_relationship (remove_relationship ["id" ])
526
647
logger .info ("detached relationship %d" % remove_relationship ["id" ])
527
648
except Exception as e :
528
649
log = "Error (%s) deleting relationship %s" % (str (e ), existing_object [mapping ["@key" ]])
0 commit comments