Skip to content

Commit e1cc4b3

Browse files
committed
Create Device42 to FreshService Script
0 parents  commit e1cc4b3

7 files changed

+818
-0
lines changed

LICENSE

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright [2019] [Device42, Inc.]
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[Device42](http://www.device42.com/) is a Continuous Discovery software for your IT Infrastructure. It helps you automatically maintain an up-to-date inventory of your physical, virtual, and cloud servers and containers, network components, software/services/applications, and their inter-relationships and inter-dependencies.
2+
3+
4+
This repository contains script that helps you sync data from Device42 to FreshService.
5+
6+
### Download and Installation
7+
-----------------------------
8+
To utilize the Device42_freshservice_mapping script, Python 3.5+ is required. The following Python Packages are required as well:
9+
10+
* pycrypto==2.6.1
11+
* pyparsing==2.1.10
12+
* pyzmq==16.0.2
13+
* requests==2.13.0
14+
* xmljson==0.2.0
15+
16+
These can all be installed by running `pip install -r requirements.txt`.
17+
18+
Once installed, the script itself is run by this command: `python d42_sd_sync.py`.
19+
20+
### Configuration
21+
-----------------------------
22+
Prior to using the script, it must be configured to connect to your Device42 instance and your FreshService instance.
23+
* Save a copy of mapping.xml.sample as mapping.xml.
24+
* Enter your URL, User, Password, API Key in the FreshService and Device42 sections (lines 2-10).
25+
API Key can be obtained from FreshService profile page
26+
27+
Below the credential settings, you’ll see a Tasks section.
28+
Multiple Tasks can be setup to synchronize various CIs from Device42 to FreshService.
29+
In the <api> section of each task, there will be a <resource> section that queries Device42 to obtain the desired CIs.
30+
Full documentation of the Device42 API and endpoints is available at https://api.device42.com.
31+
Individual tasks within a mapping.xml file can be enabled or disabled at will by changing the `enable="true"` to `enable="false"` in the <task> section.
32+
33+
Once the Device42 API resource and FreshService Target are entered, the <mapping> section is where fields from Device42 (the `resource` value) can be mapped to fields in FreshService (the `target` value).
34+
It is very important to adjust the list of default values in accordance between freshservice and device 42 (for example, service_level).
35+
36+
After configuring the fields to map as needed, the script should be ready to run.
37+
38+
### Compatibility
39+
-----------------------------
40+
* Script runs on Linux and Windows
41+
42+
### Info
43+
-----------------------------
44+
* mapping.xml - file from where we get fields relations between D42 and FreshService
45+
* devicd42.py - file with integration device42 instance
46+
* freshservice.py - file with integration freshservice instance
47+
* d42_sd_sync.py - initialization and processing file, where we prepare API calls
48+
49+
### Support
50+
-----------------------------
51+
We will support any issues you run into with the script and help answer any questions you have. Please reach out to us at [email protected]
52+
53+
###Version
54+
-----------------------------
55+
1.0.0.190411

d42_sd_sync.py

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
__author__ = 'Roman Nyschuk'
2+
3+
import os
4+
import sys
5+
import logging
6+
import json
7+
import argparse
8+
import datetime
9+
from device42 import Device42
10+
from freshservice import FreshService
11+
import xml.etree.ElementTree as eTree
12+
from xmljson import badgerfish as bf
13+
import time
14+
15+
logger = logging.getLogger('log')
16+
logger.setLevel(logging.INFO)
17+
ch = logging.StreamHandler(sys.stdout)
18+
ch.setFormatter(logging.Formatter('%(asctime)-15s\t%(levelname)s\t %(message)s'))
19+
logger.addHandler(ch)
20+
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
21+
22+
parser = argparse.ArgumentParser(description="freshservice")
23+
24+
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug output')
25+
parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode - outputs only errors')
26+
parser.add_argument('-c', '--config', help='Config file', default='mapping.xml')
27+
parser.add_argument('-l', '--logfolder', help='log folder path', default='.')
28+
29+
freshservice = None
30+
31+
32+
class JSONEncoder(json.JSONEncoder):
33+
def default(self, o):
34+
if isinstance(o, datetime):
35+
return o.strftime("%Y %m %d %H:%M:%S")
36+
return json.JSONEncoder.default(self, o)
37+
38+
39+
def find_object_by_name(assets, name):
40+
for asset in assets:
41+
if asset["name"] == name:
42+
return asset
43+
44+
return None
45+
46+
47+
def get_asset_type_field(asset_type_fields, map_info):
48+
for section in asset_type_fields:
49+
if section["field_header"] == map_info["@target-header"]:
50+
for field in section["fields"]:
51+
name = map_info["@target"]
52+
if "@target-field" in map_info:
53+
name = map_info["@target-field"]
54+
if field["asset_type_id"] is not None:
55+
name += "_" + str(field["asset_type_id"])
56+
if field["name"] == name:
57+
return field
58+
59+
return None
60+
61+
62+
def get_map_value_from_device42(source, map_info, b_add=False, asset_type_id=None):
63+
d42_value = source[map_info["@resource"]]
64+
if d42_value is None and "@resource-secondary" in map_info:
65+
d42_value = source[map_info["@resource-secondary"]]
66+
if "@is-array" in map_info and map_info["@is-array"]:
67+
d42_vals = d42_value
68+
d42_value = None
69+
for d42_val in d42_vals:
70+
if map_info["@sub-key"] in d42_val:
71+
d42_value = d42_val[map_info["@sub-key"]]
72+
break
73+
else:
74+
if "value-mapping" in map_info:
75+
d42_val = None
76+
if isinstance(map_info["value-mapping"]["item"], list):
77+
items = map_info["value-mapping"]["item"]
78+
else:
79+
items = [map_info["value-mapping"]["item"]]
80+
for item in items:
81+
if item["@key"] == d42_value:
82+
d42_val = item["@value"]
83+
if d42_val is None and "@default" in map_info["value-mapping"]:
84+
d42_val = map_info["value-mapping"]["@default"]
85+
86+
d42_value = d42_val
87+
else:
88+
pass
89+
90+
if "@target-foregin-key" in map_info:
91+
value = freshservice.get_id_by_name(map_info["@target-foregin"], d42_value)
92+
if b_add and value is None and "@not-null" in map_info and map_info[
93+
"@not-null"] and "@required" in map_info and map_info["@required"]:
94+
name = d42_value
95+
id = freshservice.insert_and_get_id_by_name(map_info["@target-foregin"], name, asset_type_id)
96+
d42_value = id
97+
else:
98+
d42_value = value
99+
100+
return d42_value
101+
102+
103+
def update_objects_from_server(sources, _target, mapping, doql=False):
104+
global freshservice
105+
106+
logger.info("Getting all existing devices in FS.")
107+
existing_objects = freshservice.request(_target["@path"] + "?include=type_fields", "GET", _target["@model"])
108+
logger.info("finished getting all existing devices in FS.")
109+
110+
asset_type = freshservice.get_ci_type_by_name(_target["@asset-type"])
111+
112+
asset_type_fields = freshservice.get_asset_type_fields(asset_type["id"])
113+
114+
for source in sources:
115+
try:
116+
existing_object = find_object_by_name(existing_objects, source["name"])
117+
data = dict()
118+
data["type_fields"] = dict()
119+
for map_info in mapping["field"]:
120+
asset_type_field = get_asset_type_field(asset_type_fields, map_info)
121+
if asset_type_field is None:
122+
continue
123+
124+
value = get_map_value_from_device42(source, map_info)
125+
126+
if asset_type_field["asset_type_id"] is not None:
127+
data["type_fields"][asset_type_field["name"]] = value
128+
else:
129+
data[map_info["@target"]] = value
130+
131+
# validation
132+
for map_info in mapping["field"]:
133+
asset_type_field = get_asset_type_field(asset_type_fields, map_info)
134+
if asset_type_field is None:
135+
continue
136+
137+
if asset_type_field["asset_type_id"] is not None:
138+
value = data["type_fields"][asset_type_field["name"]]
139+
else:
140+
value = data[map_info["@target"]]
141+
142+
is_valid = True
143+
if value is not None and "@min-length" in map_info and len(value) < map_info["@min-length"]:
144+
is_valid = False
145+
if value is None and "@not-null" in map_info and map_info["@not-null"]:
146+
is_valid = False
147+
if not is_valid and "@required" in map_info and map_info["@required"]:
148+
value = get_map_value_from_device42(source, map_info, True, data["asset_type_id"])
149+
if value is not None:
150+
is_valid = True
151+
if "@target-type" in map_info and value is not None:
152+
target_type = map_info["@target-type"]
153+
if target_type == "integer":
154+
try:
155+
value = int(value)
156+
except:
157+
is_valid = False
158+
159+
if not is_valid:
160+
logger.debug("argument '%s' is invalid." % map_info["@target"])
161+
if asset_type_field["asset_type_id"] is not None:
162+
data["type_fields"].pop(asset_type_field["name"], None)
163+
else:
164+
data.pop(map_info["@target"], None)
165+
if is_valid:
166+
if asset_type_field["asset_type_id"] is not None:
167+
data["type_fields"][asset_type_field["name"]] = value
168+
else:
169+
data[map_info["@target"]] = value
170+
171+
if existing_object is None:
172+
logger.info("adding device %s" % source["name"])
173+
new_asset_id = freshservice.insert_asset(data)
174+
logger.info("added new asset %d" % new_asset_id)
175+
else:
176+
logger.info("updating device %s" % source["name"])
177+
updated_asset_id = freshservice.update_asset(data, existing_object["display_id"])
178+
logger.info("updated new asset %d" % updated_asset_id)
179+
except Exception as e:
180+
logger.exception("Error (%s) updating device %s" % (type(e), source["name"]))
181+
182+
183+
def delete_objects_from_server(sources, _target, mapping):
184+
global freshservice
185+
186+
logger.info("Getting all existing devices in FS.")
187+
existing_objects = freshservice.request(_target["@path"] + "?include=type_fields", "GET", _target["@model"])
188+
logger.info("finished getting all existing devices in FS.")
189+
190+
for existing_object in existing_objects:
191+
exist = False
192+
for source in sources:
193+
if source[mapping["@key"]] == existing_object[mapping["@key"]]:
194+
exist = True
195+
break
196+
197+
if not exist:
198+
try:
199+
logger.info("deleting device %s" % existing_object["name"])
200+
freshservice.delete_asset(existing_object["display_id"])
201+
logger.info("deleted asset %s" % existing_object["name"])
202+
except Exception as e:
203+
logger.exception("Error (%s) deleting device %s" % (type(e), existing_object["name"]))
204+
205+
206+
def parse_config(url):
207+
config = eTree.parse(url)
208+
meta = config.getroot()
209+
config_json = bf.data(meta)
210+
211+
return config_json
212+
213+
214+
def task_execute(task, device42):
215+
if "@description" in task:
216+
logger.info("Execute task - %s" % task["@description"])
217+
218+
_resource = task["api"]["resource"]
219+
_target = task["api"]["target"]
220+
221+
method = _resource['@method']
222+
if "@doql" in _resource:
223+
doql = _resource['@doql']
224+
else:
225+
doql = None
226+
227+
source_url = _resource['@path']
228+
if "@extra-filter" in _resource:
229+
source_url += _resource["@extra-filter"] + "&amp;"
230+
231+
_type = None
232+
if "@type" in task:
233+
_type = task["@type"]
234+
235+
mapping = task['mapping']
236+
237+
if doql is not None and doql:
238+
sources = device42.doql(source_url, method, query=doql)
239+
else:
240+
sources = device42.request(source_url, method, _resource["@model"])
241+
242+
if "@delete" in _target and _target["@delete"]:
243+
delete_objects_from_server(sources, _target, mapping)
244+
return
245+
246+
update_objects_from_server(sources, _target, mapping, doql=False)
247+
248+
249+
def main():
250+
global freshservice
251+
252+
args = parser.parse_args()
253+
if args.debug:
254+
logger.setLevel(logging.DEBUG)
255+
if args.quiet:
256+
logger.setLevel(logging.ERROR)
257+
258+
try:
259+
log_file = "%s/d42_fs_sync_%d.log" % (args.logfolder, int(time.time()))
260+
logging.basicConfig(filename=log_file)
261+
except Exception as e:
262+
print("Error in config log: %s" % str(e))
263+
return -1
264+
265+
config = parse_config(args.config)
266+
logger.debug("configuration info: %s" % (json.dumps(config)))
267+
268+
settings = config["meta"]["settings"]
269+
device42 = Device42(settings['device42']['@url'], settings['device42']['@user'], settings['device42']['@pass'])
270+
freshservice = FreshService(settings['freshservice']['@url'], settings['freshservice']['@api_key'], logger)
271+
272+
if not "task" in config["meta"]["tasks"]:
273+
logger.debug("No task")
274+
return 0
275+
276+
if isinstance(config["meta"]["tasks"]["task"], list):
277+
tasks = config["meta"]["tasks"]["task"]
278+
else:
279+
tasks = [config["meta"]["tasks"]["task"]]
280+
281+
for task in tasks:
282+
if not task["@enable"]:
283+
continue
284+
285+
task_execute(task, device42)
286+
287+
print("Completed! View log at %s" % log_file)
288+
return 0
289+
290+
291+
if __name__ == "__main__":
292+
print('Running...')
293+
ret_val = main()
294+
print('Done')
295+
sys.exit(ret_val)

0 commit comments

Comments
 (0)