Skip to content

Commit a0c9a7e

Browse files
Merge pull request #8 from cscaglioned42/master
D42-29213 - GitHub Freshservice - API V1 is being deprecated Updated relationship v1 API to v2.
2 parents b229f91 + 2f7d8a1 commit a0c9a7e

File tree

3 files changed

+171
-54
lines changed

3 files changed

+171
-54
lines changed

d42_sd_sync.py

+147-26
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import xml.etree.ElementTree as eTree
1212
from xmljson import badgerfish as bf
1313
import time
14+
import math
1415

1516
logger = logging.getLogger('log')
1617
logger.setLevel(logging.INFO)
@@ -19,6 +20,15 @@
1920
logger.addHandler(ch)
2021
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
2122

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+
2232
parser = argparse.ArgumentParser(description="freshservice")
2333

2434
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug output')
@@ -225,11 +235,21 @@ def update_objects_from_server(sources, _target, mapping):
225235
data[map_info["@target"]] = value
226236

227237
if existing_object is None:
228-
logger.info("adding device %s" % source["name"])
238+
logger.info("adding asset %s" % source["name"])
229239
new_asset_id = freshservice.insert_asset(data)
230240
logger.info("added new asset %d" % new_asset_id)
231241
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"]
233253
updated_asset_id = freshservice.update_asset(data, existing_object["display_id"])
234254
logger.info("updated new asset %d" % updated_asset_id)
235255

@@ -384,16 +404,20 @@ def create_relationships_from_affinity_group(sources, _target, mapping):
384404
logger.info("finished getting all existing devices in FS.")
385405

386406
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"])
389409
logger.info("finished getting relationship type in FS.")
390410
if relationship_type is None:
391411
log = "There is no relationship type in FS. (%s - %s)" % (
392-
mapping["@forward-relationship"], mapping["@backward-relationship"])
412+
mapping["@downstream-relationship"], mapping["@upstream-relationship"])
393413
logger.info(log)
394414
return
395415

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):
397421
try:
398422
logger.info("Processing %s - %s." % (source[mapping["@key"]], source[mapping["@target-key"]]))
399423
primary_asset = find_object_by_name(existing_objects, source[mapping["@key"]])
@@ -413,25 +437,122 @@ def create_relationships_from_affinity_group(sources, _target, mapping):
413437
exist = False
414438
for relationship in relationships:
415439
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"]:
417441
exist = True
418442
break
419443
if exist:
420444
logger.info("There is already relationship in FS.")
421445
continue
422446

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[:]
431462
except Exception as e:
432463
log = "Error (%s) creating relationship %s" % (str(e), source[mapping["@key"]])
433464
logger.exception(log)
434465

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+
}
435556

436557
def delete_relationships_from_affinity_group(sources, _target, mapping):
437558
global freshservice
@@ -441,12 +562,12 @@ def delete_relationships_from_affinity_group(sources, _target, mapping):
441562
logger.info("finished getting all existing devices in FS.")
442563

443564
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"])
446567
logger.info("finished getting relationship type in FS.")
447568
if relationship_type is None:
448569
log = "There is no relationship type in FS. (%s - %s)" % (
449-
mapping["@forward-relationship"], mapping["@backward-relationship"])
570+
mapping["@downstream-relationship"], mapping["@upstream-relationship"])
450571
logger.info(log)
451572
return
452573

@@ -468,14 +589,14 @@ def delete_relationships_from_affinity_group(sources, _target, mapping):
468589
remove_relationship = None
469590
for relationship in relationships:
470591
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"]:
472593
remove_relationship = relationship
473594
break
474595
if remove_relationship is None:
475596
logger.info("There is no relationship in FS.")
476597
continue
477598

478-
freshservice.detach_relationship(primary_asset["display_id"], remove_relationship["id"])
599+
freshservice.detach_relationship(remove_relationship["id"])
479600
logger.info("detached relationship %d" % remove_relationship["id"])
480601
except Exception as e:
481602
log = "Error (%s) deleting relationship %s" % (str(e), source[mapping["@key"]])
@@ -494,12 +615,12 @@ def delete_relationships_from_business_app(sources, _target, mapping):
494615
logger.info("finished getting all existing devices in FS.")
495616

496617
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"])
499620
logger.info("finished getting relationship type in FS.")
500621
if relationship_type is None:
501622
log = "There is no relationship type in FS. (%s - %s)" % (
502-
mapping["@forward-relationship"], mapping["@backward-relationship"])
623+
mapping["@downstream-relationship"], mapping["@upstream-relationship"])
503624
logger.info(log)
504625
return
505626

@@ -509,9 +630,9 @@ def delete_relationships_from_business_app(sources, _target, mapping):
509630
relationships = freshservice.get_relationships_by_id(existing_object["display_id"])
510631
for relationship in relationships:
511632
if relationship["relationship_type_id"] == relationship_type["id"] and \
512-
relationship["relationship_type"] == "forward_relationship":
633+
relationship["primary_id"] == existing_object["display_id"]:
513634
remove_relationship = relationship
514-
target_display_id = relationship["config_item"]["display_id"]
635+
target_display_id = relationship["secondary_id"]
515636
for source in sources:
516637
if source[mapping["@key"]] == existing_object["name"]:
517638
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):
522643
if remove_relationship is None:
523644
continue
524645

525-
freshservice.detach_relationship(existing_object["display_id"], remove_relationship["id"])
646+
freshservice.detach_relationship(remove_relationship["id"])
526647
logger.info("detached relationship %d" % remove_relationship["id"])
527648
except Exception as e:
528649
log = "Error (%s) deleting relationship %s" % (str(e), existing_object[mapping["@key"]])

freshservice.py

+20-24
Original file line numberDiff line numberDiff line change
@@ -263,37 +263,28 @@ def request(self, source_url, method, model):
263263
return models
264264
return []
265265

266-
def get_relationship_type_by_content(self, forward, backward):
267-
path = "/cmdb/relationship_types/list.json"
268-
relationships = None
269-
if relationships is None:
270-
relationships = self._get(path)
266+
def get_relationship_type_by_content(self, downstream, upstream):
267+
path = "/api/v2/relationship_types"
268+
relationship_types = self.request(path, "GET", "relationship_types")
271269

272-
for relationship in relationships:
273-
if relationship["forward_relationship"] == forward and relationship["backward_relationship"] == backward:
274-
return relationship
270+
for relationship_type in relationship_types:
271+
if relationship_type["downstream_relation"] == downstream and relationship_type["upstream_relation"] == upstream:
272+
return relationship_type
275273

276274
return None
277275

278276
def get_relationships_by_id(self, asset_id):
279-
path = "/cmdb/items/%d/relationships.json" % asset_id
280-
result = self._get(path)
281-
if "relationships" in result:
282-
return result["relationships"]
283-
return []
284-
285-
def insert_relationship(self, asset_id, data):
286-
path = "/cmdb/items/%d/associate.json" % asset_id
287-
relationships = self._post(path, data)
288-
if len(relationships) > 0:
289-
return relationships[0]["id"]
290-
291-
return -1
277+
path = "/api/v2/assets/%d/relationships" % asset_id
278+
return self.request(path, "GET", "relationships")
292279

293-
def detach_relationship(self, asset_id, relationship_id):
294-
path = "/cmdb/items/%d/detach_relationship.json" % asset_id
280+
def insert_relationships(self, data):
281+
path = "/api/v2/relationships/bulk-create"
282+
job = self._post(path, data)
283+
return job["job_id"]
295284

296-
return self._delete(path, {"relationship_id": relationship_id})
285+
def detach_relationship(self, relationship_id):
286+
path = "/api/v2/relationships?ids=%d" % relationship_id
287+
return self._delete(path)
297288

298289
def get_installations_by_id(self, display_id):
299290
path = "/api/v2/applications/%d/installations" % display_id
@@ -320,3 +311,8 @@ def insert_installation(self, display_id, data):
320311
return installation['installation']["id"]
321312

322313
return -1
314+
315+
def get_job(self, job_id):
316+
path = "/api/v2/jobs/%s" % job_id
317+
return self._get(path)
318+

0 commit comments

Comments
 (0)