forked from grokability/jamf2snipe
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjamf2snipe mobile devices
290 lines (264 loc) · 12.8 KB
/
jamf2snipe mobile devices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/Library/Frameworks/Python.framework/Versions/3.7/bin/python3
# jamf2snipe - Inventory Import
#
# ABOUT:
# This program is designed to import inventory information from a
# JAMFPro into snipe-it using api calls. For more information
# about both of these products, please visit their respecitive
# websites:
# https://jamf.com
# https://snipeitapp.com
#
# LICENSE:
# GLPv3
#
# CONFIGURATION:
# These settings are commonly found in the settings.conf file.
#
# This setting sets the Snipe Asset status when creating a new asset. By default it's set to 4 (Pending).
# defaultStatus = 4
#
# You can associate snipe hardware keys in the [api-mapping] section, to to a JAMF keys so it associates
# the jamf values into snipe. The default example associates information that exists by default in both
# Snipe and JAMF. The Key value is the exact name of the snipe key name.
# Value1 is the "Subset" (JAMF's wording not mine) name, and the Value2 is the JAMF key name.
# Note that MAC Address are a custom value in SNIPE by default and you can use it as an example.
#
# [api-mapping]
# name = general name
# _snipeit_mac_address_1 = general mac_address
# _snipeit_custom_name_1234567890 = subset jamf_key
#
# A list of valid subsets are:
validsubset = (
"general",
"location",
"purchasing",
"peripherals",
"hardware",
"certificates",
"software",
"extension_attributes",
"groups_accounts",
"iphones",
"configuration_profiles",
"mobile_device"
)
# Import all the things
import json
import requests
import time
import configparser
# Find a valid settings.conf file.
config = configparser.ConfigParser()
config.read("/opt/jamf2snipe/settings.conf")
if 'snipe-it' not in set(config):
print("No valid config: /opt/jamf2snipe/settings.conf")
config.read('/etc/jamf2snipe/settings.conf')
if 'snipe-it' not in set(config):
print("No valid config: /etc/jamf2snipe/settings.conf")
config.read("settings.conf")
if 'snipe-it' not in set(config):
print("No valid config found in current folder")
raise SystemExit("Error: Unable to find valid settings.conf file. Exiting.")
# Set some Variables:
# This is the address, cname, or FQDN for your JamfPro instance.
jamfpro_base = config['jamf']['url']
jamf_api_user = config['jamf']['username']
jamf_api_password = config['jamf']['password']
# This is the address, cname, or FQDN for your snipe-it instance.
snipe_base = config['snipe-it']['url']
apiKey = config['snipe-it']['apiKey']
defaultStatus = config['snipe-it']['defaultStatus']
apple_manufacturer_id = config['snipe-it']['manufacturer_id']
# Headers for the API call.
jamfheaders = {'Accept': 'application/json'}
snipeheaders = {'Authorization': 'Bearer {}'.format(apiKey),'Accept': 'application/json','Content-Type':'application/json'}
# Check the config file for valid jamf subsets.
print("Checking the conf file")
for key in config['api-mapping']:
jamfsplit = config['api-mapping'][key].split()
if jamfsplit[0] in validsubset:
print('Found subset {}: Good'.format(jamfsplit[0]))
continue
else:
print("Found subset {}: This is not in the acceptable list of subsets. Check your settings.conf\n Valid subsets are:".format(jamfsplit[0]))
print(validsubset)
raise SystemExit("Invalid Subset found in settings.conf")
### Setup Some Functions ###
# Function to make the API call for all JAMF devices
def get_jamf_computers():
api_url = '{0}/JSSResource/mobiledevices'.format(jamfpro_base)
response = requests.get(api_url, auth=(jamf_api_user, jamf_api_password), headers=jamfheaders)
if response.status_code == 200:
return response.json()
else:
print('Error code:{}'.format(response.status_code))
return None
# Function to lookup a JAMF asset by id.
def search_jamf_asset(jamf_id):
api_url = "{}/JSSResource/mobiledevices/id/{}".format(jamfpro_base, jamf_id)
response = requests.get(api_url, auth=(jamf_api_user, jamf_api_password), headers=jamfheaders)
if response.status_code == 200:
jsonresponse = response.json()
return jsonresponse['mobile_device']
elif b'policies.ratelimit.QuotaViolation' in response.content:
print('JAMFPro responded with error code: {} - Policy Ratelimit Quota Violation - when we tried to look up id: {} Waiting a bit to retry the lookup.'.format(response, jamf_id))
time.sleep(75)
newresponse = search_jamf_asset(jamf_id, asset_tag)
return newresponse
else:
print('JAMFPro responded with error code:{} when we tried to look up id: {}'.format(response, jamf_id))
return None
# Function to update the asset tag in JAMF with an number passed from Snipe.
def update_jamf_asset_tag(jamf_id, asset_tag):
api_url = "{}/JSSResource/mobiledevices/id/{}".format(jamfpro_base, jamf_id)
payload = """<?xml version="1.0" encoding="UTF-8"?><mobile_device><general><id>{}</id><asset_tag>{}</asset_tag></general></mobile_device>""".format(jamf_id, asset_tag)
response = requests.put(api_url, auth=(jamf_api_user, jamf_api_password), data=payload, headers=jamfheaders)
if response.status_code == 201:
return True
elif b'policies.ratelimit.QuotaViolation' in response.content:
print('JAMFPro responded with error code: {} - Policy Ratelimit Quota Violation - when we tried to look up id: {} Waiting a bit to retry the lookup.'.format(response, jamf_id))
time.sleep(75)
newupdate = update_jamf_asset_tag(jamf_id, asset_tag)
return newupdate
else:
print('JAMFPro responded with error code:{} when we tried to update id: {}'.format(response, jamf_id))
return False
# Function to lookup a snipe asset by serial number or other identifier.
def search_snipe_asset(search_term):
api_url = '{}/api/v1/hardware?search={}'.format(snipe_base, search_term)
response = requests.get(api_url, headers=snipeheaders)
if response.status_code == 200:
jsonresponse = response.json()
# Check to make sure there's actually a result
if jsonresponse['total'] == 1:
return jsonresponse
elif jsonresponse['total'] == 0:
print("No assets match {}".format(search_term))
return "NoMatch"
else:
print('WARNING: FOUND {} matching assets while searching for: {}'.format(jsonresponse['total'], search_term))
return "MultiMatch"
else:
print('Snipe-IT responded with error code:{} when we tried to look up: {}'.format(response, search_term))
return "ERROR"
# Function to get all the asset models
def get_snipe_models():
api_url = '{}/api/v1/models'.format(snipe_base)
# print ('Calling against: {}'.format(api_url))
response = requests.get(api_url, headers=snipeheaders)
if response.status_code == 200:
return response.json()
else:
print('Error code:{}'.format(response.status_code))
return None
# Function that creates a new Snipe Model - not an asset - with a JSON payload
def create_snipe_model(payload):
api_url = '{}/api/v1/models'.format(snipe_base)
response = requests.post(api_url, headers=snipeheaders, json=payload)
if response.status_code == 200:
jsonresponse = response.json()
modelnumbers[jsonresponse['payload']['model_number']] = jsonresponse['payload']['id']
return True
else:
print('Error code: {} while trying to create a new model.'.format(response.status_code))
return False
# Function to create a new asset by passing array
def create_snipe_asset(payload):
api_url = '{}/api/v1/hardware'.format(snipe_base)
#print ('Calling against: {}'.format(api_url))
response = requests.post(api_url, headers=snipeheaders, json=payload)
if response.status_code == 200:
#print(response.content)
return "AssetCreated"
else:
return response
# Function that updates a snipe asset with a JSON payload
def update_snipe_asset(snipe_id, payload):
api_url = '{}/api/v1/hardware/{}'.format(snipe_base, snipe_id)
response = requests.patch(api_url, headers=snipeheaders, json=payload)
# Verify that the payload updated properly.
if response.status_code == 200:
check = json.dumps(next(iter(payload.values())))
bytecheck = check.encode('utf-8')
if bytecheck not in response.content:
print('Error: Did not update ID: {}. The payload was: {}'.format(snipe_id, json.dumps(payload)))
return response
else:
return "AssetUpdated"
else:
print('Whoops. Got an error code while updating ID {}: {}'.format(snipe_id, response.status_code))
return response
### Get Started ###
# Get a list of known models from Snipe
snipemodels = get_snipe_models()
modelnumbers = {}
for model in snipemodels['rows']:
modelnumbers[model['model_number']] = model['id']
# Get the IDS of all active assets.
jamf_computer_list = get_jamf_computers()
# Make sure we have a good list.
if jamf_computer_list is not None:
print('Received a list of JAMF assets.\nUpdating inventory')
for jamf_asset in jamf_computer_list['mobile_devices']:
# Search through the list by ID for all asset information
jamf = search_jamf_asset(jamf_asset['id'])
if jamf is None:
continue
# Check that the model number exists in snipe, if not create it.
if jamf['general']['model_identifier'] not in modelnumbers:
newmodel = {"category_id":3,"manufacturer_id":apple_manufacturer_id,"name": jamf['general']['model'],"model_number":jamf['general']['model_identifier']}
create_snipe_model(newmodel)
# Pass the SN from JAMF to search for a match in Snipe
snipe = search_snipe_asset(jamf['general']['serial_number'])
# Create a new asset if there's no match:
if snipe is 'NoMatch':
print("Creating a new asset in snipe.")
# This section checks to see if the asset tag was already put into JAMF, if not it creates one with with Jamf's ID.
if jamf['general']['asset_tag'] is '':
jamf_asset_tag = 'jamfid-{}'.format(jamf['general']['id'])
else:
jamf_asset_tag = jamf['general']['asset_tag']
# Create the payload
newasset = {'asset_tag': jamf_asset_tag,'model_id': modelnumbers['{}'.format(jamf['general']['model_identifier'])], 'name': jamf['general']['name'], 'status_id': 1,'serial': jamf['general']['serial_number']}
if jamf['general']['serial_number'] == 'Not Available':
print("Serial is not available, skipping.")
else:
create_snipe_asset(newasset)
# Log an error if there's an issue, or more than once match.
elif snipe is 'MultiMatch':
print("ERROR: You need to resolve multiple assets with the same serial number in your inventory. If you can't find them in your inventory, you might need to purge your deleted records. You can find that in the Snipe Admin settings. Skipping this serial number for now.")
elif snipe is 'ERROR':
print("Check your snipe instance and setup. Skipping for now.")
else:
# Only update if JAMF has more recent info.
snipe_id = snipe['rows'][0]['id']
snipe_time = snipe['rows'][0]['updated_at']['datetime']
jamf_time = jamf['general']['report_date']
# Check to see that the JAMF record is newer than the previous snipe update.
if jamf_time > snipe_time:
for snipekey in config['api-mapping']:
jamfsplit = config['api-mapping'][snipekey].split()
payload = {snipekey: jamf['{}'.format(jamfsplit[0])]['{}'.format(jamfsplit[1])]}
update_snipe_asset(snipe_id, payload)
# Update/Sync the Snipe Asset Tag Number back to JAMF
if jamf['general']['asset_tag'] is not snipe['rows'][0]['asset_tag']:
if snipe['rows'][0]['asset_tag'][0].isdigit():
update_jamf_asset_tag("{}".format(jamf['general']['id']), '{}'.format(snipe['rows'][0]['asset_tag']))
### Some Error Investigation ###
else:
print("Couldn't get a list of computers from JAMF. Maybe your username and password are bad?")
# Do some tests to see if the hosts are up.
try:
SNIPE_UP = True if requests.get(snipe_base).status_code is 200 else False
except:
SNIPE_UP = False
try:
JAMF_UP = True if requests.get(jamf_base).status_code is 200 or 401 else False
except:
JAMF_UP = False
if SNIPE_UP is False:
print('Snipe-IT looks like it is down from here. Please check your config or your instance.')
if SNIPE_UP is False:
print('JAMFPro looks down from here. Please check the your config or your hosted JAMFPro instance.')