From 164200410753bd7e4d6386efeceadc0eb06da52b Mon Sep 17 00:00:00 2001 From: Roman Nyschuk Date: Mon, 23 Nov 2020 13:45:20 -0500 Subject: [PATCH 1/5] Update from freshservice app --- README.md | 47 +++++- d42_sd_sync.py | 390 +++++++++++++++++++++++++++++++++------------ freshservice.py | 84 +++++++--- mapping.xml.sample | 299 +++++++++++++++++++++++++++++----- 4 files changed, 657 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index c5e1aec..36232a4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ This repository contains script that helps you sync data from Device42 to FreshS ### Download and Installation ----------------------------- -To utilize the Device42_freshservice_mapping script, Python 3.5+ is required. The following Python Packages are required as well: +Device42 v16.19.00+ (Legacy Included) +Python 3.5+ + +The following Python Packages are required: * pycrypto==2.6.1 * pyparsing==2.1.10 @@ -17,6 +20,28 @@ These can all be installed by running `pip install -r requirements.txt`. Once installed, the script itself is run by this command: `python d42_sd_sync.py`. + +### Download and Installation (Legacy) +----------------------------- +To utilize the Device42_freshservice_mapping script, Python 3.5+ is required. The following Python Packages are required as well: + +pycrypto==2.6.1 +pyparsing==2.1.10 +pyzmq==16.0.2 +requests==2.13.0 +xmljson==0.2.0 +These can all be installed by running pip install -r requirements.txt. + +In order to run the legacy migration, you will also need to modify the mapping.xml file so that the legacy mapping options are used + +modify the following line so that `enable` is set to false for the v2_views +```enable="false" description="Copy Servers from Device42 to FreshService using DOQL v2_views"``` + +modify the following line so that `enable` is set to true for the v1_views +```enable="true" description="Copy Servers from Device42 to FreshService using DOQL v1_views"``` + +Once the packages are installed and the script is configured, the script can be run by this command: python d42_sd_sync.py. + ### Configuration ----------------------------- Prior to using the script, it must be configured to connect to your Device42 instance and your FreshService instance. @@ -35,6 +60,22 @@ It is very important to adjust the list of default values in accordance between After configuring the fields to map as needed, the script should be ready to run. +### Gotchas +----------------------------- +* FreshService API Limit is 1000 calls per hour (https://api.freshservice.com/#ratelimit) +* Due to the nature of FreshService rate limits, large inventories may take extended periods of time to migrate + +Please use the following table as a reference only, actual times may vary due to request limit cooldowns and other internal API calls + +|# of Devices| Migration Time| +|------------|---------------| +| 100 | 6 min | +| 1,000 | 1 hr | +| 5,000 | 5 hrs | +|10,000 | 10 hrs | +|24,000 | 24 hrs | + + ### Compatibility ----------------------------- * Script runs on Linux and Windows @@ -50,6 +91,6 @@ After configuring the fields to map as needed, the script should be ready to run ----------------------------- We will support any issues you run into with the script and help answer any questions you have. Please reach out to us at support@device42.com -###Version +### Version ----------------------------- -1.0.0.190411 \ No newline at end of file +2.0.0.201021 \ No newline at end of file diff --git a/d42_sd_sync.py b/d42_sd_sync.py index 129565f..6de4d9e 100755 --- a/d42_sd_sync.py +++ b/d42_sd_sync.py @@ -7,7 +7,7 @@ import argparse import datetime from device42 import Device42 -from freshservice import FreshService +from freshservice import FreshService, FreshServiceDuplicateSerialError import xml.etree.ElementTree as eTree from xmljson import badgerfish as bf import time @@ -38,7 +38,7 @@ def default(self, o): def find_object_by_name(assets, name): for asset in assets: - if asset["name"] == name: + if asset["name"].lower() == name.lower(): return asset return None @@ -81,7 +81,19 @@ def get_map_value_from_device42(source, map_info, b_add=False, asset_type_id=Non if item["@key"] == d42_value: d42_val = item["@value"] if d42_val is None and "@default" in map_info["value-mapping"]: - d42_val = map_info["value-mapping"]["@default"] + default_value = map_info["value-mapping"]["@default"] + + # If we send a software status of "", we get the following error from the API: + # Error 400: {"description":"Validation failed","errors":[{"field":"status", + # "message":"It should be one of these values: 'blacklisted,ignored,managed'","code":"invalid_value"}]} + # So if we have a value in D42 that does not map to Freshservice (we don't currently have a value that + # does not map), instead of clearing the value in Freshservice by sending a "", it will try to set that + # value and this is not one of the available options. However, if we set the software status to None, + # the value in Freshservice will get cleared. + if default_value == "null": + d42_val = None + else: + d42_val = default_value d42_value = d42_val else: @@ -90,9 +102,12 @@ def get_map_value_from_device42(source, map_info, b_add=False, asset_type_id=Non if "@target-foregin-key" in map_info: value = freshservice.get_id_by_name(map_info["@target-foregin"], d42_value) if b_add and value is None and "@not-null" in map_info and map_info[ - "@not-null"]: # and "@required" in map_info and map_info["@required"] + "@not-null"]: # and "@required" in map_info and map_info["@required"] if d42_value is not None: - name = d42_value + if "@max-length" in map_info and len(d42_value) > map_info["@max-length"]: + name = d42_value[0:map_info["@max-length"] - 3] + "..." + else: + name = d42_value if map_info["@target-foregin"] == "vendors": new_id = freshservice.insert_and_get_id_by_name(map_info["@target-foregin"], name, None) else: @@ -101,7 +116,11 @@ def get_map_value_from_device42(source, map_info, b_add=False, asset_type_id=Non else: d42_value = None else: - d42_value = value + # If value is None, that means we could not find a match for the D42 value in Freshservice. + # We will return the same D42 value since for product we will call this function again with + # the required asset_type_id which is needed to create the value in Freshservice. + if value is not None: + d42_value = value return d42_value @@ -111,82 +130,119 @@ def update_objects_from_server(sources, _target, mapping): logger.info("Getting all existing devices in FS.") existing_objects = freshservice.request(_target["@path"] + "?include=type_fields", "GET", _target["@model"]) - logger.info("finished getting all existing devices in FS.") + logger.info("Finished getting all existing devices in FS.") asset_type = freshservice.get_ci_type_by_name(_target["@asset-type"]) asset_type_fields = freshservice.get_asset_type_fields(asset_type["id"]) for source in sources: - try: - existing_object = find_object_by_name(existing_objects, source["name"]) - data = dict() - data["type_fields"] = dict() - for map_info in mapping["field"]: - asset_type_field = get_asset_type_field(asset_type_fields, map_info) - if asset_type_field is None: - continue - - value = get_map_value_from_device42(source, map_info) + error_skip = False + while True: + try: + existing_object = find_object_by_name(existing_objects, source["name"]) + data = dict() + data["type_fields"] = dict() + for map_info in mapping["field"]: + if error_skip and "@error-skip" in map_info and map_info["@error-skip"]: + continue + asset_type_field = get_asset_type_field(asset_type_fields, map_info) + if asset_type_field is None: + continue + + value = get_map_value_from_device42(source, map_info) - if asset_type_field["asset_type_id"] is not None: - data["type_fields"][asset_type_field["name"]] = value - else: - data[map_info["@target"]] = value + if asset_type_field["asset_type_id"] is not None: + data["type_fields"][asset_type_field["name"]] = value + else: + data[map_info["@target"]] = value - # validation - for map_info in mapping["field"]: - asset_type_field = get_asset_type_field(asset_type_fields, map_info) - if asset_type_field is None: - continue + # validation + for map_info in mapping["field"]: + if error_skip and "@error-skip" in map_info and map_info["@error-skip"]: + continue - if asset_type_field["asset_type_id"] is not None: - value = data["type_fields"][asset_type_field["name"]] - else: - value = data[map_info["@target"]] - - is_valid = True - if value is not None and "@min-length" in map_info and len(value) < map_info["@min-length"]: - is_valid = False - if value == "" and "@set-space" in map_info and map_info["@set-space"]: - is_valid = True - value = " " * map_info["@min-length"] - if value is None and "@not-null" in map_info and map_info["@not-null"]: - is_valid = False - if not is_valid and "@target-foregin-key" in map_info: - value = get_map_value_from_device42(source, map_info, True, data["asset_type_id"]) - if value is not None: - is_valid = True - if "@target-type" in map_info and value is not None: - target_type = map_info["@target-type"] - if target_type == "integer": - try: - value = int(value) - except: - is_valid = False + asset_type_field = get_asset_type_field(asset_type_fields, map_info) + if asset_type_field is None: + continue - if not is_valid: - logger.debug("argument '%s' is invalid." % map_info["@target"]) if asset_type_field["asset_type_id"] is not None: - data["type_fields"].pop(asset_type_field["name"], None) + value = data["type_fields"][asset_type_field["name"]] else: - data.pop(map_info["@target"], None) - if is_valid: - if asset_type_field["asset_type_id"] is not None: - data["type_fields"][asset_type_field["name"]] = value - else: - data[map_info["@target"]] = value + value = data[map_info["@target"]] + + is_valid = True + if value is not None and "@min-length" in map_info and len(value) < map_info["@min-length"]: + is_valid = False + if value == "" and "@set-space" in map_info and map_info["@set-space"]: + is_valid = True + value = " " * map_info["@min-length"] + # value might have been translated to an associated ID in Freshservice by get_map_value_from_device42 + # which is why we need to check that value is a string using isinstance. + if value is not None and "@max-length" in map_info and isinstance(value, str) and len(value) > map_info["@max-length"]: + value = value[0:map_info["@max-length"]-3] + "..." + if value is None and "@not-null" in map_info and map_info["@not-null"]: + if map_info["@target"] == "asset_tag": + is_valid = False + else: + # There is an issue with the Freshservice API where sending a null value for + # a field will result in the API returning an error like "Has 0 characters, + # it should have minimum of 1 characters and can have maximum of 255 characters". + # This prevents us from being able to clear these field values in Freshservice (even though + # the Freshservice UI allows you to clear these fields). To get around this, we will send + # a single space for string values and a 0 for integer and float values when the value + # coming from D42 is null. + if "@target-type" in map_info: + target_type = map_info["@target-type"] + if target_type == "integer" or target_type == "float": + value = 0 + else: + value = " " + else: + value = " " + + if "@target-foregin-key" in map_info: + value = get_map_value_from_device42(source, map_info, True, data["asset_type_id"]) + is_valid = value is not None + if "@target-type" in map_info and value is not None: + target_type = map_info["@target-type"] + if target_type == "integer": + try: + value = int(value) + except: + is_valid = False + + if not is_valid: + logger.debug("argument '%s' is invalid." % map_info["@target"]) + if asset_type_field["asset_type_id"] is not None: + data["type_fields"].pop(asset_type_field["name"], None) + else: + data.pop(map_info["@target"], None) + if is_valid: + if asset_type_field["asset_type_id"] is not None: + data["type_fields"][asset_type_field["name"]] = value + else: + data[map_info["@target"]] = value + + if existing_object is None: + logger.info("adding device %s" % source["name"]) + new_asset_id = freshservice.insert_asset(data) + logger.info("added new asset %d" % new_asset_id) + else: + logger.info("updating device %s" % source["name"]) + updated_asset_id = freshservice.update_asset(data, existing_object["display_id"]) + logger.info("updated new asset %d" % updated_asset_id) - if existing_object is None: - logger.info("adding device %s" % source["name"]) - new_asset_id = freshservice.insert_asset(data) - logger.info("added new asset %d" % new_asset_id) - else: - logger.info("updating device %s" % source["name"]) - updated_asset_id = freshservice.update_asset(data, existing_object["display_id"]) - logger.info("updated new asset %d" % updated_asset_id) - except Exception as e: - logger.exception("Error (%s) updating device %s" % (type(e), source["name"])) + break + except FreshServiceDuplicateSerialError: + if not error_skip: + error_skip = True + continue + break + except Exception as e: + log = "Error (%s) updating device %s" % (str(e), source["name"]) + logger.exception(log) + break def delete_objects_from_server(sources, _target, mapping): @@ -209,7 +265,115 @@ def delete_objects_from_server(sources, _target, mapping): freshservice.delete_asset(existing_object["display_id"]) logger.info("deleted asset %s" % existing_object["name"]) except Exception as e: - logger.exception("Error (%s) deleting device %s" % (type(e), existing_object["name"])) + log = "Error (%s) deleting device %s" % (str(e), existing_object["name"]) + logger.exception(log) + + +def update_softwares_from_server(sources, _target, mapping): + global freshservice + + logger.info("Getting all existing softwares in FS.") + existing_objects = freshservice.request(_target["@path"], "GET", _target["@model"]) + logger.info("finished getting all existing softwares in FS.") + + for source in sources: + try: + existing_object = find_object_by_name(existing_objects, source["name"]) + data = dict() + for map_info in mapping["field"]: + value = get_map_value_from_device42(source, map_info) + + # value might have been translated to an associated ID in Freshservice by get_map_value_from_device42 + # which is why we need to check that value is a string using isinstance. + if value is not None and "@max-length" in map_info and isinstance(value, str) and len(value) > map_info["@max-length"]: + value = value[0:map_info["@max-length"] - 3] + "..." + + data[map_info["@target"]] = value + + if existing_object is None: + logger.info("adding software %s" % source["name"]) + new_software_id = freshservice.insert_software(data) + logger.info("added new software %d" % new_software_id) + else: + logger.info("updating software %s" % source["name"]) + updated_software_id = freshservice.update_software(data, existing_object["id"]) + logger.info("updated new software %d" % updated_software_id) + except Exception as e: + log = "Error (%s) updating software %s" % (str(e), source["name"]) + logger.exception(log) + + +def delete_softwares_from_server(sources, _target, mapping): + global freshservice + + logger.info("Getting all existing softwares in FS.") + existing_objects = freshservice.request(_target["@path"] + "?include=type_fields", "GET", _target["@model"]) + logger.info("finished getting all existing devices in FS.") + + for existing_object in existing_objects: + exist = False + for source in sources: + if source[mapping["@key"]] == existing_object[mapping["@key"]]: + exist = True + break + + if not exist: + try: + logger.info("deleting software %s" % existing_object["name"]) + freshservice.delete_software(existing_object["id"]) + logger.info("deleted software %s" % existing_object["name"]) + except Exception as e: + log = "Error (%s) deleting software %s" % (str(e), existing_object["name"]) + logger.exception(log) + + +def create_installation_from_software_in_use(sources, _target, mapping): + global freshservice + + logger.info("Getting all existing devices in FS.") + existing_objects = freshservice.request("api/v2/assets", "GET", "assets") + logger.info("finished getting all existing devices in FS.") + + logger.info("Getting all existing softwares in FS.") + existing_softwares = freshservice.request("api/v2/applications", "GET", "applications") + logger.info("finished getting all existing softwares in FS.") + + for source in sources: + try: + logger.info("Processing %s - %s." % (source[mapping["@device-name"]], source[mapping["@software-name"]])) + asset = find_object_by_name(existing_objects, source[mapping["@device-name"]]) + software = find_object_by_name(existing_softwares, source[mapping["@software-name"]]) + + if asset is None: + log = "There is no asset(%s) in FS." % source[mapping["@device-name"]] + logger.exception(log) + continue + + if software is None: + log = "There is no software(%s) in FS." % source[mapping["@software-name"]] + logger.exception(log) + continue + + installations = freshservice.get_installations_by_id(software["id"]) + exist = False + for installation in installations: + if installation["installation_machine_id"] == asset["display_id"]: + exist = True + break + if exist: + logger.info("There is already installation in FS.") + continue + + data = dict() + data["installation_machine_id"] = asset["display_id"] + data["version"] = source[mapping["@version"]] + data["installation_date"] = source[mapping["@install-date"]] + logger.info("adding installation %s-%s" % (source[mapping["@device-name"]], source[mapping["@software-name"]])) + freshservice.insert_installation(software["id"], data) + logger.info("added installation %s-%s" % (source[mapping["@device-name"]], source[mapping["@software-name"]])) + except Exception as e: + log = "Error (%s) creating installation %s" % (str(e), source[mapping["@device-name"]]) + logger.exception(log) def create_relationships_from_affinity_group(sources, _target, mapping): @@ -217,7 +381,6 @@ def create_relationships_from_affinity_group(sources, _target, mapping): logger.info("Getting all existing devices in FS.") existing_objects = freshservice.request("api/v2/assets" + "?include=type_fields", "GET", _target["@model"]) - logger.info("finished getting all existing devices in FS.") logger.info("Getting relationship type in FS.") @@ -225,8 +388,9 @@ def create_relationships_from_affinity_group(sources, _target, mapping): mapping["@backward-relationship"]) logger.info("finished getting relationship type in FS.") if relationship_type is None: - logger.info("There is not relationship type in FS. (%s - %s)" % ( - mapping["@forward-relationship"], mapping["@backward-relationship"])) + log = "There is no relationship type in FS. (%s - %s)" % ( + mapping["@forward-relationship"], mapping["@backward-relationship"]) + logger.info(log) return for source in sources: @@ -236,11 +400,13 @@ def create_relationships_from_affinity_group(sources, _target, mapping): secondary_asset = find_object_by_name(existing_objects, source[mapping["@target-key"]]) if primary_asset is None: - logger.info("There is no dependent asset(%s) in FS." % source[mapping["@key"]]) + log = "There is no dependent asset(%s) in FS." % source[mapping["@key"]] + logger.exception(log) continue if secondary_asset is None: - logger.info("There is no dependency asset(%s) in FS." % source[mapping["@target-key"]]) + log = "There is no dependency asset(%s) in FS." % source[mapping["@target-key"]] + logger.exception(log) continue relationships = freshservice.get_relationships_by_id(primary_asset["display_id"]) @@ -263,14 +429,15 @@ def create_relationships_from_affinity_group(sources, _target, mapping): new_relationship_id = freshservice.insert_relationship(primary_asset["display_id"], data) logger.info("added new relationship %d" % new_relationship_id) except Exception as e: - logger.exception("Error (%s) creating relationship %s" % (type(e), source[mapping["@key"]])) + log = "Error (%s) creating relationship %s" % (str(e), source[mapping["@key"]]) + logger.exception(log) def delete_relationships_from_affinity_group(sources, _target, mapping): global freshservice + logger.info("Getting all existing devices in FS.") existing_objects = freshservice.request("api/v2/assets" + "?include=type_fields", "GET", _target["@model"]) - logger.info("finished getting all existing devices in FS.") logger.info("Getting relationship type in FS.") @@ -278,8 +445,9 @@ def delete_relationships_from_affinity_group(sources, _target, mapping): mapping["@backward-relationship"]) logger.info("finished getting relationship type in FS.") if relationship_type is None: - logger.info("There is not relationship type in FS. (%s - %s)" % ( - mapping["@forward-relationship"], mapping["@backward-relationship"])) + log = "There is no relationship type in FS. (%s - %s)" % ( + mapping["@forward-relationship"], mapping["@backward-relationship"]) + logger.info(log) return for source in sources: @@ -304,13 +472,14 @@ def delete_relationships_from_affinity_group(sources, _target, mapping): remove_relationship = relationship break if remove_relationship is None: - logger.info("There is not relationship in FS.") + logger.info("There is no relationship in FS.") continue freshservice.detach_relationship(primary_asset["display_id"], remove_relationship["id"]) logger.info("detached relationship %d" % remove_relationship["id"]) except Exception as e: - logger.exception("Error (%s) creating relationship %s" % (type(e), source[mapping["@key"]])) + log = "Error (%s) deleting relationship %s" % (str(e), source[mapping["@key"]]) + logger.exception(log) def create_relationships_from_business_app(sources, _target, mapping): @@ -319,9 +488,9 @@ def create_relationships_from_business_app(sources, _target, mapping): def delete_relationships_from_business_app(sources, _target, mapping): global freshservice + logger.info("Getting all existing devices in FS.") existing_objects = freshservice.request("api/v2/assets" + "?include=type_fields", "GET", _target["@model"]) - logger.info("finished getting all existing devices in FS.") logger.info("Getting relationship type in FS.") @@ -329,30 +498,35 @@ def delete_relationships_from_business_app(sources, _target, mapping): mapping["@backward-relationship"]) logger.info("finished getting relationship type in FS.") if relationship_type is None: - logger.info("There is not relationship type in FS. (%s - %s)" % ( - mapping["@forward-relationship"], mapping["@backward-relationship"])) + log = "There is no relationship type in FS. (%s - %s)" % ( + mapping["@forward-relationship"], mapping["@backward-relationship"]) + logger.info(log) return for existing_object in existing_objects: - logger.info("Checking relationship of asset(%s)." % existing_object["name"]) - relationships = freshservice.get_relationships_by_id(existing_object["display_id"]) - for relationship in relationships: - if relationship["relationship_type_id"] == relationship_type["id"] and \ - relationship["relationship_type"] == "forward_relationship": - remove_relationship = relationship - target_display_id = relationship["config_item"]["display_id"] - for source in sources: - if source[mapping["@key"]] == existing_object["name"]: - secondary_asset = find_object_by_name(existing_objects, source[mapping["@target-key"]]) - if target_display_id == secondary_asset["display_id"]: - remove_relationship = None - break - - if remove_relationship is None: - continue - - freshservice.detach_relationship(existing_object["display_id"], remove_relationship["id"]) - logger.info("detached relationship %d" % remove_relationship["id"]) + try: + logger.info("Checking relationship of asset(%s)." % existing_object["name"]) + relationships = freshservice.get_relationships_by_id(existing_object["display_id"]) + for relationship in relationships: + if relationship["relationship_type_id"] == relationship_type["id"] and \ + relationship["relationship_type"] == "forward_relationship": + remove_relationship = relationship + target_display_id = relationship["config_item"]["display_id"] + for source in sources: + if source[mapping["@key"]] == existing_object["name"]: + secondary_asset = find_object_by_name(existing_objects, source[mapping["@target-key"]]) + if target_display_id == secondary_asset["display_id"]: + remove_relationship = None + break + + if remove_relationship is None: + continue + + freshservice.detach_relationship(existing_object["display_id"], remove_relationship["id"]) + logger.info("detached relationship %d" % remove_relationship["id"]) + except Exception as e: + log = "Error (%s) deleting relationship %s" % (str(e), existing_object[mapping["@key"]]) + logger.exception(log) def parse_config(url): @@ -401,6 +575,16 @@ def task_execute(task, device42): delete_relationships_from_business_app(sources, _target, mapping) else: create_relationships_from_business_app(sources, _target, mapping) + elif _type == "software": + if "@delete" in _target and _target["@delete"]: + delete_softwares_from_server(sources, _target, mapping) + else: + update_softwares_from_server(sources, _target, mapping) + elif _type == "software_in_use": + if "@delete" in _target and _target["@delete"]: + delete_softwares_from_server(sources, _target, mapping) + else: + create_installation_from_software_in_use(sources, _target, mapping) else: if "@delete" in _target and _target["@delete"]: delete_objects_from_server(sources, _target, mapping) diff --git a/freshservice.py b/freshservice.py index 2eba3ce..3e8b071 100755 --- a/freshservice.py +++ b/freshservice.py @@ -12,15 +12,11 @@ class FreshServiceBaseException(Exception): pass -class FreshServiceBadArgumentError(Exception): - pass - - class FreshServiceHTTPError(FreshServiceBaseException): pass -class FreshServiceWrongRequest(FreshServiceHTTPError): +class FreshServiceDuplicateSerialError(FreshServiceHTTPError): pass @@ -36,10 +32,9 @@ def __init__(self, endpoint, api_key, logger, **kwargs): self.base_url = "https://%s" % self.base self.headers = {} self.last_time_call_api = None - self.period_call_api = 4 + self.period_call_api = 1 self.api_call_count = 0 self.asset_types = None - self.server_data = dict() def _send(self, method, path, data=None): """ General method to send requests """ @@ -50,9 +45,9 @@ def _send(self, method, path, data=None): if method == 'GET' and data is not None and "page" in data: is_getting_exist = True - if not is_getting_exist and self.last_time_call_api is not None and ( - now - self.last_time_call_api).total_seconds() < self.period_call_api: - time.sleep(self.period_call_api - (now - self.last_time_call_api).total_seconds()) + # if not is_getting_exist and self.last_time_call_api is not None and ( + # now - self.last_time_call_api).total_seconds() < self.period_call_api: + # time.sleep(self.period_call_api - (now - self.last_time_call_api).total_seconds()) url = "%s/%s" % (self.base_url, path) params = None @@ -80,6 +75,22 @@ def _send(self, method, path, data=None): time.sleep(60) continue + if resp.status_code == 400: + exception = None + try: + error_resp = resp.json() + if error_resp["description"] == "Validation failed": + for error in error_resp["errors"]: + if error["field"] == "serial_number" and error["message"] == " must be unique": + exception = FreshServiceDuplicateSerialError("HTTP %s (%s) Error %s: %s\n request was %s" % + (method, path, resp.status_code, resp.text, data)) + break + except Exception: + pass + + if exception is not None: + raise exception + raise FreshServiceHTTPError("HTTP %s (%s) Error %s: %s\n request was %s" % (method, path, resp.status_code, resp.text, data)) @@ -132,6 +143,21 @@ def get_assets_by_asset_type(self, asset_type_id): assets = self._get(path) return assets["assets"] + def insert_software(self, data): + path = "api/v2/applications" + result = self._post(path, data) + return result["application"]["id"] + + def update_software(self, data, id): + path = "api/v2/applications/%d" % id + result = self._put(path, data) + return result["application"]["id"] + + def delete_software(self, id): + path = "api/v2/applications/%d" % id + result = self._delete(path) + return result + def get_all_ci_types(self): if self.asset_types is not None: return self.asset_types @@ -215,18 +241,12 @@ def insert_and_get_id_by_name(self, model, name, asset_type_id): data = {"name": name} models = self._post(path, data) for key in models: - if model in self.server_data: - self.server_data[model] += [models[key]] - return models[key]["id"] return None def request(self, source_url, method, model): if method == "GET": - if model in self.server_data: - return self.server_data[model] - models = [] page = 1 while True: @@ -239,19 +259,15 @@ def request(self, source_url, method, model): break page += 1 - self.server_data[model] = models + return models return [] def get_relationship_type_by_content(self, forward, backward): path = "/cmdb/relationship_types/list.json" relationships = None - if "relationships" in self.server_data: - relationships = self.server_data["relationships"] - if relationships is None: relationships = self._get(path) - self.server_data["relationships"] = relationships for relationship in relationships: if relationship["forward_relationship"] == forward and relationship["backward_relationship"] == backward: @@ -278,3 +294,29 @@ def detach_relationship(self, asset_id, relationship_id): path = "/cmdb/items/%d/detach_relationship.json" % asset_id return self._delete(path, {"relationship_id": relationship_id}) + + def get_installations_by_id(self, display_id): + path = "/api/v2/applications/%d/installations" % display_id + + models = [] + page = 1 + while True: + result = self._get(path, data={"page": page}) + if "installations" in result: + models += result["installations"] + if len(result["installations"]) == 0: + break + else: + break + + page += 1 + + return models + + def insert_installation(self, display_id, data): + path = "/api/v2/applications/%d/installations" % display_id + installation = self._post(path, data) + if len(installation) > 0: + return installation['installation']["id"] + + return -1 diff --git a/mapping.xml.sample b/mapping.xml.sample index 5434db6..5281afb 100644 --- a/mapping.xml.sample +++ b/mapping.xml.sample @@ -10,74 +10,242 @@ - + - + - + - + target-header="General" not-null="true" min-length="1" max-length="255"/> + - - - + + target-header="Cost" target-type="float" not-null="true" set-zero="true"/> + target-header="Computer" not-null="true" max-length="255"/> + target-header="Computer" not-null="true" max-length="255"/> - - - - - - - - + target-header="Computer" not-null="true" set-space="true" min-length="1" max-length="255"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + target-foregin="products" target-foregin-key="name" not-null="true" required="True" max-length="255"/> + target-header="Hardware" not-null="true" set-space="true" + min-length="1" max-length="255" error-skip="true"/> + + + + + + @@ -90,12 +258,54 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -107,7 +317,6 @@ @@ -115,15 +324,24 @@ - + - + @@ -131,8 +349,17 @@ From a7729edf6505413509afaa002338d3aa65671bef Mon Sep 17 00:00:00 2001 From: Roman Nyschuk Date: Mon, 7 Dec 2020 11:34:43 -0500 Subject: [PATCH 2/5] Remove unused info in mapping file --- mapping.xml.sample | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/mapping.xml.sample b/mapping.xml.sample index 5281afb..dab94dc 100644 --- a/mapping.xml.sample +++ b/mapping.xml.sample @@ -69,7 +69,7 @@ /> - + @@ -121,15 +121,10 @@ - - - - - - + @@ -187,7 +182,7 @@ /> - + @@ -241,10 +236,6 @@ - - - - @@ -266,7 +257,7 @@ doql="select * from (select trim(name) as name, min(view_software_v1.software_type) as software_type, min(view_software_v1.notes) as notes, 'desktop' as application_type, min(view_software_v1.software_type) as category, min(last_changed) as last_changed from view_software_v1 group by trim(name)) a" /> - + - + From 045d231600ff2e22f3d418a2a451bf746da410f4 Mon Sep 17 00:00:00 2001 From: Roman Nyschuk Date: Mon, 7 Dec 2020 16:19:21 -0500 Subject: [PATCH 3/5] Update versin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36232a4..9b2441f 100644 --- a/README.md +++ b/README.md @@ -93,4 +93,4 @@ We will support any issues you run into with the script and help answer any ques ### Version ----------------------------- -2.0.0.201021 \ No newline at end of file +2.0.0.201207 \ No newline at end of file From e62af505c7f0022e5c6e0f703b383b3590f70213 Mon Sep 17 00:00:00 2001 From: Roman Nyschuk Date: Wed, 9 Dec 2020 11:40:24 -0500 Subject: [PATCH 4/5] Add path attr at resource --- mapping.xml.sample | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mapping.xml.sample b/mapping.xml.sample index dab94dc..48aa2e8 100644 --- a/mapping.xml.sample +++ b/mapping.xml.sample @@ -15,6 +15,7 @@ @@ -319,6 +323,7 @@