xolo-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +7 -0
- data/bin/xoloserver +106 -0
- data/data/com.pixar.xoloserver.plist +29 -0
- data/data/uninstall-pkgs-by-id.zsh +103 -0
- data/lib/xolo/server/app.rb +133 -0
- data/lib/xolo/server/command_line.rb +216 -0
- data/lib/xolo/server/configuration.rb +739 -0
- data/lib/xolo/server/constants.rb +70 -0
- data/lib/xolo/server/helpers/auth.rb +257 -0
- data/lib/xolo/server/helpers/client_data.rb +415 -0
- data/lib/xolo/server/helpers/file_transfers.rb +265 -0
- data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
- data/lib/xolo/server/helpers/log.rb +97 -0
- data/lib/xolo/server/helpers/maintenance.rb +401 -0
- data/lib/xolo/server/helpers/notification.rb +145 -0
- data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
- data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
- data/lib/xolo/server/helpers/title_editor.rb +92 -0
- data/lib/xolo/server/helpers/titles.rb +145 -0
- data/lib/xolo/server/helpers/versions.rb +160 -0
- data/lib/xolo/server/log.rb +286 -0
- data/lib/xolo/server/mixins/changelog.rb +315 -0
- data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
- data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
- data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
- data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
- data/lib/xolo/server/object_locks.rb +102 -0
- data/lib/xolo/server/routes/auth.rb +89 -0
- data/lib/xolo/server/routes/jamf_pro.rb +89 -0
- data/lib/xolo/server/routes/maint.rb +174 -0
- data/lib/xolo/server/routes/title_editor.rb +71 -0
- data/lib/xolo/server/routes/titles.rb +285 -0
- data/lib/xolo/server/routes/uploads.rb +93 -0
- data/lib/xolo/server/routes/versions.rb +261 -0
- data/lib/xolo/server/routes.rb +168 -0
- data/lib/xolo/server/title.rb +1143 -0
- data/lib/xolo/server/version.rb +902 -0
- data/lib/xolo/server.rb +205 -0
- data/lib/xolo-server.rb +8 -0
- metadata +243 -0
|
@@ -0,0 +1,1668 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Mixins
|
|
16
|
+
|
|
17
|
+
# This is mixed in to Xolo::Server::Title
|
|
18
|
+
# to define Title-related access to the Jamf Pro server
|
|
19
|
+
#
|
|
20
|
+
module TitleJamfAccess
|
|
21
|
+
|
|
22
|
+
# Constants
|
|
23
|
+
#
|
|
24
|
+
##############################
|
|
25
|
+
##############################
|
|
26
|
+
|
|
27
|
+
# Module methods
|
|
28
|
+
#
|
|
29
|
+
# These are available as module methods but not as 'helper'
|
|
30
|
+
# methods in sinatra routes & views.
|
|
31
|
+
#
|
|
32
|
+
##############################
|
|
33
|
+
##############################
|
|
34
|
+
|
|
35
|
+
# when this module is included
|
|
36
|
+
##############################
|
|
37
|
+
def self.included(includer)
|
|
38
|
+
Xolo.verbose_include includer, self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Instance methods
|
|
42
|
+
#
|
|
43
|
+
# These are available directly in title objects
|
|
44
|
+
#
|
|
45
|
+
##############################
|
|
46
|
+
##############################
|
|
47
|
+
|
|
48
|
+
####### The Xolo Title itself
|
|
49
|
+
###########################################
|
|
50
|
+
###########################################
|
|
51
|
+
|
|
52
|
+
# @return [String] The start of the Jamf Pro URL for GUI/WebApp access
|
|
53
|
+
################
|
|
54
|
+
def jamf_gui_url
|
|
55
|
+
server_app_instance.jamf_gui_url
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create title-level things in jamf when creating a title.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
################################
|
|
62
|
+
def create_title_in_jamf
|
|
63
|
+
# ORDER MATTERS
|
|
64
|
+
|
|
65
|
+
# create the normal ea if needed
|
|
66
|
+
configure_jamf_normal_ea if version_script
|
|
67
|
+
|
|
68
|
+
# must happen after the normal ea is created
|
|
69
|
+
configure_jamf_installed_group
|
|
70
|
+
|
|
71
|
+
if uninstall_script || !uninstall_ids.pix_empty?
|
|
72
|
+
configure_jamf_uninstall_script
|
|
73
|
+
# this creates the policy to use the script
|
|
74
|
+
# must happen after the uninstall script is created
|
|
75
|
+
jamf_uninstall_policy
|
|
76
|
+
|
|
77
|
+
# this creates the expire policy if needed
|
|
78
|
+
jamf_expire_policy if expiration && !expire_paths.pix_empty?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create the static group that will contain computers where this title is 'frozen'
|
|
82
|
+
# Just calling this will create it if it doesn't exist.
|
|
83
|
+
jamf_frozen_group
|
|
84
|
+
|
|
85
|
+
activate_jamf_patch_title
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Apply any changes to Jamf as needed
|
|
89
|
+
# Mostly this just sets flags indicating what needs to be updated in the
|
|
90
|
+
# various version-related things in jamf - policies, self service, etc.
|
|
91
|
+
#
|
|
92
|
+
# @return [void]
|
|
93
|
+
#########################
|
|
94
|
+
def update_title_in_jamf
|
|
95
|
+
# ORDER MATTERS
|
|
96
|
+
|
|
97
|
+
# do we have a version_script? if so we maintain a 'normal' EA
|
|
98
|
+
# this has to happen before updating the installed_group
|
|
99
|
+
configure_jamf_normal_ea if need_to_update_jamf_normal_ea?
|
|
100
|
+
|
|
101
|
+
# this smart group might use the normal-EA or might use app data
|
|
102
|
+
# If those have changed, we need to update it.
|
|
103
|
+
configure_jamf_installed_group if need_to_update_jamf_installed_group?
|
|
104
|
+
|
|
105
|
+
# if the exclusions have changed update the manual install released policy
|
|
106
|
+
if changes_for_update[:excluded_groups]
|
|
107
|
+
progress "Jamf: Updating excluded groups for Manual Released Policy '#{jamf_manual_install_released_policy_name}'."
|
|
108
|
+
configure_jamf_manual_install_released_policy(jamf_manual_install_released_policy)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Do we need to update (vs delete) the uninstall script?
|
|
112
|
+
if need_to_update_jamf_uninstall_script?
|
|
113
|
+
|
|
114
|
+
configure_jamf_uninstall_script
|
|
115
|
+
configure_jamf_uninstall_policy
|
|
116
|
+
|
|
117
|
+
# this creates the policy to use the script, if needed
|
|
118
|
+
# which needs both the uninstall script and the installed group to exist
|
|
119
|
+
jamf_uninstall_policy
|
|
120
|
+
|
|
121
|
+
# or delete it if no longer needed
|
|
122
|
+
elsif need_to_delete_jamf_uninstall_script?
|
|
123
|
+
delete_jamf_uninstall_policy
|
|
124
|
+
delete_jamf_uninstall_script
|
|
125
|
+
# can't expire without
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Do we need to add or delete the expire policy?
|
|
129
|
+
# NOTE: if the uninstall script was deleted,
|
|
130
|
+
# expiration won't do anything.
|
|
131
|
+
if need_to_update_expiration?
|
|
132
|
+
changes_for_update.dig(:expiration, :new).to_i.positive? ? jamf_expire_policy : delete_jamf_expire_policy
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# If we don't use a version script anymore, delete the normal EA
|
|
136
|
+
# this has to happen after updating the installed_group
|
|
137
|
+
delete_jamf_normal_ea unless version_script_contents
|
|
138
|
+
|
|
139
|
+
update_description_in_jamf
|
|
140
|
+
update_ssvc
|
|
141
|
+
update_ssvc_category
|
|
142
|
+
# TODO: deal with icon changes: if changes_for_update&.key? :self_service_icon
|
|
143
|
+
|
|
144
|
+
if jamf_ted_title_active?
|
|
145
|
+
update_versions_for_title_changes_in_jamf
|
|
146
|
+
else
|
|
147
|
+
log_debug "Jamf: Title '#{display_name}' (#{title}) is not yet active to Jamf, nothing to update in versions."
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Repair this title in Jamf Pro
|
|
152
|
+
# - TODO: activate title in patch mgmt
|
|
153
|
+
# - TODO: Accept Patch EA
|
|
154
|
+
# - Normal EA 'xolo-<title>-installed-version'
|
|
155
|
+
# - title-installed smart group 'xolo-<title>-installed'
|
|
156
|
+
# - frozen static group 'xolo-<title>-frozen'
|
|
157
|
+
# - manual/SSvc install-current-release policy 'xolo-<title>-install'
|
|
158
|
+
# - trigger 'xolo-<title>-install'
|
|
159
|
+
# - ssvc icon
|
|
160
|
+
# - ssvc category
|
|
161
|
+
# - description
|
|
162
|
+
# - if uninstallable
|
|
163
|
+
# - uninstall script 'xolo-<title>-uninstall'
|
|
164
|
+
# - uninstall policy 'xolo-<title>-uninstall'
|
|
165
|
+
# - if expirable
|
|
166
|
+
# - expire policy 'xolo-<title>-expire'
|
|
167
|
+
# - trigger 'xolo-<title>-expire'
|
|
168
|
+
#
|
|
169
|
+
###############################################
|
|
170
|
+
def repair_jamf_title_objects
|
|
171
|
+
progress "Jamf: Repairing Jamf objects for title '#{title}'", log: :info
|
|
172
|
+
repair_jamf_normal_ea
|
|
173
|
+
configure_jamf_installed_group
|
|
174
|
+
repair_jamf_uninstall_policy
|
|
175
|
+
repair_jamf_uninstall_script
|
|
176
|
+
repair_jamf_expire_policy
|
|
177
|
+
repair_frozen_group
|
|
178
|
+
repair_jamf_manual_install_released_policy
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Delete an entire title from Jamf Pro
|
|
182
|
+
# alway delete policies first, then scripts, then groups, then EAs, then the patch title
|
|
183
|
+
########################
|
|
184
|
+
def delete_title_from_jamf
|
|
185
|
+
# ORDER MATTERS
|
|
186
|
+
delete_jamf_expire_policy
|
|
187
|
+
delete_jamf_uninstall_policy
|
|
188
|
+
delete_jamf_manual_install_released_policy
|
|
189
|
+
delete_jamf_uninstall_script
|
|
190
|
+
delete_jamf_frozen_group
|
|
191
|
+
delete_jamf_installed_group
|
|
192
|
+
delete_jamf_normal_ea
|
|
193
|
+
delete_jamf_patch_title
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# If any title changes require updates to existing versions in
|
|
197
|
+
# Jamf, this loops thru the versions and applies
|
|
198
|
+
# them
|
|
199
|
+
#
|
|
200
|
+
# This should happen after the incoming changes have been applied to this
|
|
201
|
+
# title instance
|
|
202
|
+
#
|
|
203
|
+
# Jamf Stuff
|
|
204
|
+
# - update any policy scopes
|
|
205
|
+
# - update any policy SSvc settings
|
|
206
|
+
#
|
|
207
|
+
# @return [void]
|
|
208
|
+
############################
|
|
209
|
+
def update_versions_for_title_changes_in_jamf
|
|
210
|
+
version_objects.each do |vers_obj|
|
|
211
|
+
vers_obj.update_release_groups(ttl_obj: self) if changes_for_update&.key? :release_groups
|
|
212
|
+
vers_obj.update_excluded_groups(ttl_obj: self) if changes_for_update&.key? :excluded_groups
|
|
213
|
+
vers_obj.update_jamf_package_notes(ttl_obj: self) if need_to_update_description?
|
|
214
|
+
# vers_obj.update_ssvc(ttl_obj: self) if changes_for_update&.key? :self_service
|
|
215
|
+
# vers_obj.update_ssvc_category(ttl_obj: self) if changes_for_update&.key? :self_service_category
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Update the description in Jamfy places it appears
|
|
220
|
+
# At the moment, this is only the manual install policy
|
|
221
|
+
# if its in self service. The package notes are updated
|
|
222
|
+
# by the versions themselves via the
|
|
223
|
+
# update_versions_for_title_changes_in_jamf method
|
|
224
|
+
#
|
|
225
|
+
# @return [void]
|
|
226
|
+
#########################
|
|
227
|
+
def update_description_in_jamf
|
|
228
|
+
return unless need_to_update_description?
|
|
229
|
+
|
|
230
|
+
# Update the manual install policy
|
|
231
|
+
return unless self_service
|
|
232
|
+
|
|
233
|
+
pol = jamf_manual_install_released_policy
|
|
234
|
+
return unless pol
|
|
235
|
+
|
|
236
|
+
progress "Jamf: Updating Description for Self Service in policy '#{pol.name}'.", log: :info
|
|
237
|
+
new_desc = changes_for_update[:description][:new] || description
|
|
238
|
+
pol.self_service_description = new_desc
|
|
239
|
+
pol.save
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# do we need to update the description?
|
|
243
|
+
# True if our incoming changes include :description
|
|
244
|
+
#
|
|
245
|
+
# @return [Boolean]
|
|
246
|
+
###################################
|
|
247
|
+
def need_to_update_description?
|
|
248
|
+
changes_for_update.key?(:description)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# do we need to create or delete the expire policy?
|
|
252
|
+
# True if our incoming changes include :expiration
|
|
253
|
+
#
|
|
254
|
+
# Ignore the expire paths - even when disabling expiration
|
|
255
|
+
# they can stay there, they won't mean anything.
|
|
256
|
+
#
|
|
257
|
+
# @return [Boolean]
|
|
258
|
+
###################################
|
|
259
|
+
def need_to_update_expiration?
|
|
260
|
+
changes_for_update.key?(:expiration)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Get the patch report for this title.
|
|
264
|
+
# It's the JPAPI report data with each hash having a frozen: key added
|
|
265
|
+
#
|
|
266
|
+
# TODO: rework this when all the paging stuff is handled by ruby-jss
|
|
267
|
+
#
|
|
268
|
+
# @param vers [String, nil] Limit the report to a specific version
|
|
269
|
+
#
|
|
270
|
+
# @return [Arrah<Hash>] Data for each computer with any version of this title installed
|
|
271
|
+
######################
|
|
272
|
+
def patch_report(vers: nil)
|
|
273
|
+
vers = Xolo::Server::Helpers::JamfPro::PATCH_REPORT_UNKNOWN_VERSION if vers == Xolo::UNKNOWN
|
|
274
|
+
vers &&= CGI.escape vers.to_s
|
|
275
|
+
|
|
276
|
+
page_size = Xolo::Server::Helpers::JamfPro::PATCH_REPORT_JPAPI_PAGE_SIZE
|
|
277
|
+
page = 0
|
|
278
|
+
paged_rsrc = "#{patch_report_rsrc}?page=#{page}&page-size=#{page_size}"
|
|
279
|
+
paged_rsrc << "&filter=version%3D%3D#{vers}" if vers
|
|
280
|
+
|
|
281
|
+
report = []
|
|
282
|
+
loop do
|
|
283
|
+
data = jamf_cnx.jp_get(paged_rsrc)[:results]
|
|
284
|
+
log_debug "GOT #{paged_rsrc} >>> results size: #{data.size}"
|
|
285
|
+
break if data.empty?
|
|
286
|
+
|
|
287
|
+
report += data
|
|
288
|
+
page += 1
|
|
289
|
+
paged_rsrc = "#{patch_report_rsrc}?page=#{page}&page-size=#{page_size}"
|
|
290
|
+
paged_rsrc << "&filter=version%3D%3D#{vers}" if vers
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# log_debug "REPORT: #{report}"
|
|
294
|
+
|
|
295
|
+
frozen_comps = frozen_computers.keys
|
|
296
|
+
report.each do |h|
|
|
297
|
+
h[:frozen] = frozen_comps.include? h[:computerName]
|
|
298
|
+
h[:version] = Xolo::UNKNOWN if h[:version] == Xolo::Server::Helpers::JamfPro::PATCH_REPORT_UNKNOWN_VERSION
|
|
299
|
+
end
|
|
300
|
+
report
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
####### The'Normal" Extension Attribute
|
|
304
|
+
###########################################
|
|
305
|
+
###########################################
|
|
306
|
+
|
|
307
|
+
# @return [Boolean] Does the 'normal' EA exist in jamf?
|
|
308
|
+
#########################
|
|
309
|
+
def jamf_normal_ea_exist?
|
|
310
|
+
Jamf::ComputerExtensionAttribute.all_names(:refresh, cnx: jamf_cnx).include? jamf_normal_ea_name
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Create or fetch the 'normal' EA in jamf
|
|
314
|
+
# If we are deleting and it doesn't exist, return nil.
|
|
315
|
+
#
|
|
316
|
+
# @return [Jamf::ComputerExtensionAttribute] The 'normal' Jamf ComputerExtensionAttribute for this title
|
|
317
|
+
########################
|
|
318
|
+
def jamf_normal_ea
|
|
319
|
+
return @jamf_normal_ea if @jamf_normal_ea
|
|
320
|
+
|
|
321
|
+
if jamf_normal_ea_exist?
|
|
322
|
+
@jamf_normal_ea = Jamf::ComputerExtensionAttribute.fetch(name: jamf_normal_ea_name, cnx: jamf_cnx)
|
|
323
|
+
|
|
324
|
+
else
|
|
325
|
+
return if deleting?
|
|
326
|
+
|
|
327
|
+
msg = "Jamf: Creating regular extension attribute '#{jamf_normal_ea_name}' for use in smart groups"
|
|
328
|
+
progress msg, log: :info
|
|
329
|
+
|
|
330
|
+
@jamf_normal_ea = Jamf::ComputerExtensionAttribute.create(
|
|
331
|
+
name: jamf_normal_ea_name,
|
|
332
|
+
cnx: jamf_cnx
|
|
333
|
+
)
|
|
334
|
+
@jamf_normal_ea.save
|
|
335
|
+
|
|
336
|
+
end
|
|
337
|
+
@jamf_normal_ea
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Configure the 'normal' EA that matches the Patch EA for this title,
|
|
341
|
+
# so that it can be used in smart groups and adv. searches.
|
|
342
|
+
# (Patch EAs aren't available for use in smart group critera)
|
|
343
|
+
#
|
|
344
|
+
# If we have one already but are deleting it, that happens elsewhere
|
|
345
|
+
#
|
|
346
|
+
# @return [void]
|
|
347
|
+
################################
|
|
348
|
+
def configure_jamf_normal_ea
|
|
349
|
+
progress "Jamf: Configuring regular extension attribute '#{jamf_normal_ea_name}'", log: :info
|
|
350
|
+
|
|
351
|
+
jamf_normal_ea.description = "The version of xolo title '#{title}' installed on the machine"
|
|
352
|
+
jamf_normal_ea.data_type = :string
|
|
353
|
+
|
|
354
|
+
# this is our incoming or already-existing EA script
|
|
355
|
+
if version_script_contents.pix_empty?
|
|
356
|
+
# nothing to do if its nil, if we need to delete it, that'll happen later
|
|
357
|
+
else
|
|
358
|
+
jamf_normal_ea.enabled = true
|
|
359
|
+
jamf_normal_ea.input_type = 'script'
|
|
360
|
+
jamf_normal_ea.script = version_script_contents
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
jamf_normal_ea.script = scr
|
|
364
|
+
jamf_normal_ea.save
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Repair the 'normal' EA in jamf to match our version_script
|
|
368
|
+
########################
|
|
369
|
+
def repair_jamf_normal_ea
|
|
370
|
+
if version_script_contents.pix_empty?
|
|
371
|
+
delete_jamf_normal_ea
|
|
372
|
+
else
|
|
373
|
+
configure_jamf_normal_ea
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Delete the 'normal' computer ext attr matching the Patch EA
|
|
378
|
+
# @return [void]
|
|
379
|
+
######################################
|
|
380
|
+
def delete_jamf_normal_ea
|
|
381
|
+
ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
|
|
382
|
+
return unless ea_id
|
|
383
|
+
|
|
384
|
+
progress "Jamf: Deleting regular extension attribute '#{jamf_normal_ea_name}'", log: :info
|
|
385
|
+
Jamf::ComputerExtensionAttribute.delete ea_id, cnx: jamf_cnx
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# do we need to update the normal EA in jamf?
|
|
389
|
+
# true if our incoming changes include :version_script
|
|
390
|
+
# and the new value is not empty (in which case we'll delete it)
|
|
391
|
+
#
|
|
392
|
+
# @return [Boolean]
|
|
393
|
+
########################
|
|
394
|
+
def need_to_update_jamf_normal_ea?
|
|
395
|
+
changes_for_update.key?(:version_script) && !changes_for_update[:version_script][:new].pix_empty?
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# the script contents of the Normal Jamf EA that comes from our version_script
|
|
399
|
+
# @return [String, nil] nil if there is none
|
|
400
|
+
##############################
|
|
401
|
+
def jamf_normal_ea_contents
|
|
402
|
+
return unless jamf_normal_ea_exist?
|
|
403
|
+
|
|
404
|
+
jamf_normal_ea.script
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# @return [String] the URL for the Normal EA in Jamf Pro
|
|
408
|
+
######################
|
|
409
|
+
def jamf_normal_ea_url
|
|
410
|
+
return @jamf_normal_ea_url if @jamf_normal_ea_url
|
|
411
|
+
return unless version_script
|
|
412
|
+
|
|
413
|
+
ea_id = Jamf::ComputerExtensionAttribute.valid_id jamf_normal_ea_name, cnx: jamf_cnx
|
|
414
|
+
return unless ea_id
|
|
415
|
+
|
|
416
|
+
# Jamf Changed the URL!
|
|
417
|
+
# @jamf_normal_ea_url = "#{jamf_gui_url}/computerExtensionAttributes.html?id=#{ea_id}&o=r"
|
|
418
|
+
|
|
419
|
+
@jamf_normal_ea_url = "#{jamf_gui_url}/view/settings/computer-management/computer-extension-attributes/#{ea_id}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
####### The Patch Ext Attribute
|
|
423
|
+
###########################################
|
|
424
|
+
###########################################
|
|
425
|
+
|
|
426
|
+
# Do we need to accept the patch ea in jamf?
|
|
427
|
+
#
|
|
428
|
+
# True if
|
|
429
|
+
# - title activation, and version_script
|
|
430
|
+
# - any time version_script changes
|
|
431
|
+
# - any time we switch from bundle data to version_script
|
|
432
|
+
#
|
|
433
|
+
# @need_to_accept_jamf_patch_ea is set true by these methods
|
|
434
|
+
# - Title#create_ted_ea
|
|
435
|
+
# - Title#update_ted_ea
|
|
436
|
+
#
|
|
437
|
+
# @return [Boolean]
|
|
438
|
+
#########################
|
|
439
|
+
def need_to_accept_jamf_patch_ea?
|
|
440
|
+
@need_to_accept_jamf_patch_ea
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# This method should only be called when we *expect* to need to accept the EA -
|
|
444
|
+
# not only when we first activate a title with a version script, but when the version_script
|
|
445
|
+
# has changed, or been added, replacing app_name and app_bundle_id.
|
|
446
|
+
#
|
|
447
|
+
# If the EA needs acceptance when this method starts, we accept it and we're done.
|
|
448
|
+
#
|
|
449
|
+
# If not (there is no EA, or it's already accepted) then we spin off a thread that
|
|
450
|
+
# waits up to an hour for Jamf to notice the change from the Title Editor and require
|
|
451
|
+
# re-acceptance.
|
|
452
|
+
#
|
|
453
|
+
# As soon as we see that Jamf shows accepted: false, we'll accept it and be done.
|
|
454
|
+
#
|
|
455
|
+
# If we make it for an hour and never see the expected need for acceptance, we
|
|
456
|
+
# log it and send an alert about it.
|
|
457
|
+
#
|
|
458
|
+
# TODO: when this is implemented in ruby-jss, use the implementation
|
|
459
|
+
#
|
|
460
|
+
# NOTE: PATCHing the ea of the title requires CRUD privs for computer ext attrs
|
|
461
|
+
#
|
|
462
|
+
# @return [void]
|
|
463
|
+
############################
|
|
464
|
+
def accept_jamf_patch_ea
|
|
465
|
+
return unless need_to_accept_jamf_patch_ea?
|
|
466
|
+
|
|
467
|
+
# return with warning if we aren't auto-accepting
|
|
468
|
+
unless Xolo::Server.config.jamf_auto_accept_xolo_eas
|
|
469
|
+
msg = "Jamf: IMPORTANT: Before adding any versions, the Extension Attribute (version-script) for this title must be accepted manually in Jamf Pro at #{jamf_patch_ea_url} under the 'Extension Attribute' tab (click 'Edit') "
|
|
470
|
+
progress msg
|
|
471
|
+
log_debug 'Admin informed about accepting EA/version-script manually'
|
|
472
|
+
return
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# this is true if the Jamf server already knows it needs to be accepted
|
|
476
|
+
# so just do it now
|
|
477
|
+
if jamf_patch_ea_awaiting_acceptance?
|
|
478
|
+
progress "Jamf: Auto-accepting use of version-script ExtensionAttribute '#{ted_ea_key}'", log: :info
|
|
479
|
+
accept_jamf_patch_ea_via_api
|
|
480
|
+
return
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# If not, we are here because we expect it will need acceptance soon
|
|
484
|
+
# So we call this method to wait for Jamf to notice that,
|
|
485
|
+
# checking in the background for up to an hour.
|
|
486
|
+
auto_accept_patch_ea_in_thread
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Wait for up to an hour for Jamf to notice that our TEd EA needs to be
|
|
490
|
+
# accepted, and then do it
|
|
491
|
+
#####################
|
|
492
|
+
def auto_accept_patch_ea_in_thread
|
|
493
|
+
# don't do this if there's already one running for this instance
|
|
494
|
+
if @auto_accept_ea_thread&.alive?
|
|
495
|
+
log_debug "Jamf: auto_accept_ea_thread already running. Caller: #{caller_locations.first}"
|
|
496
|
+
return
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
progress "Jamf: version-script ExtAttr for this title '#{ted_ea_key}' will be auto-accepted when Jamf sees the changes in the Title Editor"
|
|
500
|
+
|
|
501
|
+
@auto_accept_ea_thread = Thread.new do
|
|
502
|
+
log_debug "Jamf: Starting auto_accept_ea_thread for #{title}"
|
|
503
|
+
start_time = Time.now
|
|
504
|
+
max_time = start_time + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
|
|
505
|
+
|
|
506
|
+
start_time = start_time.strftime '%F %T'
|
|
507
|
+
did_it = false
|
|
508
|
+
|
|
509
|
+
while Time.now < max_time
|
|
510
|
+
sleep 30
|
|
511
|
+
|
|
512
|
+
# refresh our jamf connection cuz it might expire if this takes a while, esp if using
|
|
513
|
+
# an APIClient
|
|
514
|
+
jamf_cnx(refresh: true) if jamf_cnx.token.secs_remaining < 90
|
|
515
|
+
|
|
516
|
+
log_debug "Jamf: checking for expected (re)acceptance of version-script ExtensionAttribute '#{ted_ea_key}' since #{start_time}"
|
|
517
|
+
next unless jamf_patch_ea_awaiting_acceptance?
|
|
518
|
+
|
|
519
|
+
accept_jamf_patch_ea_via_api
|
|
520
|
+
log_info "Jamf: Auto-accepted use of version-script ExtensionAttribute '#{ted_ea_key}'"
|
|
521
|
+
did_it = true
|
|
522
|
+
break
|
|
523
|
+
end # while
|
|
524
|
+
|
|
525
|
+
unless did_it
|
|
526
|
+
msg = "Jamf: Expected to (re)accept version-script ExtensionAttribute '#{ted_ea_key}', but Jamf hasn't seen the change in over #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} secs. Please investigate."
|
|
527
|
+
log_error msg, alert: true
|
|
528
|
+
end
|
|
529
|
+
end # thread
|
|
530
|
+
@auto_accept_ea_thread.name = "auto_accept_ea_thread for #{title}"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Does the Jamf Title currently need its EA to be accepted, according to Jamf Pro?
|
|
534
|
+
#
|
|
535
|
+
# NOTE: Jamf might not see the need for this immediately, so we set
|
|
536
|
+
# @need_to_accept_jamf_patch_ea and define #need_to_accept_jamf_patch_ea?
|
|
537
|
+
# and use them to determine if we should wait for this to become true.
|
|
538
|
+
#
|
|
539
|
+
# @return [Boolean]
|
|
540
|
+
#################################
|
|
541
|
+
def jamf_patch_ea_awaiting_acceptance?
|
|
542
|
+
ead = jamf_patch_ea_data
|
|
543
|
+
return unless ead
|
|
544
|
+
|
|
545
|
+
!ead[:accepted]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Does the EA for this title in Jamf match the version script we know about?
|
|
549
|
+
#
|
|
550
|
+
# If we don't have a version script, then we don't really care what Jamf has at the moment,
|
|
551
|
+
# Jamf's should go away once it catches up with the title editor.
|
|
552
|
+
#
|
|
553
|
+
# But if we do have one, and Jamf has something different, we'll need to accept it,
|
|
554
|
+
# if configured to do so automatically.
|
|
555
|
+
#
|
|
556
|
+
# This method just tells us the current situation about our version script
|
|
557
|
+
# vs the Jamf Patch EA.
|
|
558
|
+
#
|
|
559
|
+
# @param new_version_script [String, nil] If updating, this is the new incoming version script.
|
|
560
|
+
#
|
|
561
|
+
# @return [Boolean, nil] nil if we have no version script,
|
|
562
|
+
# otherwise, does jamf match our version_script?
|
|
563
|
+
#########################
|
|
564
|
+
def jamf_patch_ea_matches_version_script?
|
|
565
|
+
# our current version script - nil if we currently don't have one
|
|
566
|
+
our_version_script = version_script_contents
|
|
567
|
+
|
|
568
|
+
# we don't have one, so if Jamf does at the moment, it'll go away soon
|
|
569
|
+
# when jamf catches up with the title editor.
|
|
570
|
+
return unless our_version_script
|
|
571
|
+
|
|
572
|
+
# does jamf's script match ours?
|
|
573
|
+
our_version_script.chomp == jamf_patch_ea_contents.chomp
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# The version_script as a Jamf Extension Attribute,
|
|
577
|
+
# once the title as been activated in Jamf.
|
|
578
|
+
#
|
|
579
|
+
# This is a hash of data returned from the JP API endpoint:
|
|
580
|
+
# "v2/patch-software-title-configurations/#{jamf_patch_title_id}/extension-attributes"
|
|
581
|
+
# which has these keys:
|
|
582
|
+
#
|
|
583
|
+
# :accepted [Boolean] has it been accepted for the title?
|
|
584
|
+
#
|
|
585
|
+
# :eaId [String] the 'key' of the EA from the title editor
|
|
586
|
+
#
|
|
587
|
+
# :displayName [String] the displayname from the title editor, for titles
|
|
588
|
+
# maintained by xolo, it's the same as the eaId
|
|
589
|
+
#
|
|
590
|
+
# :scriptContent [String] the Base64-encoded script of the EA.
|
|
591
|
+
#
|
|
592
|
+
# TODO: when this gets implemented in ruby-jss, use that implementation
|
|
593
|
+
# and return the patch title ea object.
|
|
594
|
+
#
|
|
595
|
+
# NOTE: The title must be activated in Jamf before accessing this.
|
|
596
|
+
#
|
|
597
|
+
# NOTE: We fetch this hash every time this method is called, since we may
|
|
598
|
+
# be waiting for jamf to notice that the EA has changed in the Title Editor
|
|
599
|
+
# and needs re-acceptance
|
|
600
|
+
#
|
|
601
|
+
# NOTE: While Jamf Patch allows for multiple EAs per title, the Title Editor only
|
|
602
|
+
# allows for one. So even tho the data comes back in an array, we only care about
|
|
603
|
+
# the first (and only) value.
|
|
604
|
+
#
|
|
605
|
+
# @return [Hash] the data from the JPAPI endpoint,
|
|
606
|
+
# nil if the title has no EA at the moment
|
|
607
|
+
########################
|
|
608
|
+
def jamf_patch_ea_data
|
|
609
|
+
return unless jamf_patch_title_id
|
|
610
|
+
|
|
611
|
+
jamf_cnx.jp_get("v2/patch-software-title-configurations/#{jamf_patch_title_id}/extension-attributes").first
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# API call to accept the version-script EA in Jamf Pro
|
|
615
|
+
# TODO: when this gets implemented in ruby-jss, use that implementation
|
|
616
|
+
#
|
|
617
|
+
# @return [void]
|
|
618
|
+
########################
|
|
619
|
+
def accept_jamf_patch_ea_via_api
|
|
620
|
+
patchdata = <<~ENDPATCHDATA
|
|
621
|
+
{
|
|
622
|
+
"extensionAttributes": [
|
|
623
|
+
{
|
|
624
|
+
"accepted": true,
|
|
625
|
+
"eaId": "#{ted_ea_key}"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
ENDPATCHDATA
|
|
630
|
+
jamf_cnx.jp_patch "v2/patch-software-title-configurations/#{jamf_patch_title_id}", patchdata
|
|
631
|
+
log_debug "Jamf: Auto-accepted ExtensionAttribute '#{ted_ea_key}'"
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# the script contents of the Jamf Patch EA that comes from our version_script
|
|
635
|
+
# @return [String, nil] nil if there is none, or the title isn't active yet
|
|
636
|
+
##############################
|
|
637
|
+
def jamf_patch_ea_contents
|
|
638
|
+
jea_data = jamf_patch_ea_data
|
|
639
|
+
return unless jea_data && jea_data[:scriptContents]
|
|
640
|
+
|
|
641
|
+
Base64.decode64 jea_data[:scriptContents]
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# @return [String] the URL for the Patch EA in Jamf Pro
|
|
645
|
+
######################
|
|
646
|
+
def jamf_patch_ea_url
|
|
647
|
+
return @jamf_patch_ea_url if @jamf_patch_ea_url
|
|
648
|
+
return unless version_script
|
|
649
|
+
|
|
650
|
+
@jamf_patch_ea_url = "#{jamf_patch_title_url}?tab=extension"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
####### The UnInstall Script
|
|
654
|
+
###########################################
|
|
655
|
+
###########################################
|
|
656
|
+
|
|
657
|
+
# @return [Boolean] Does the uninstall script exist in jamf?
|
|
658
|
+
##########################
|
|
659
|
+
def jamf_uninstall_script_exist?
|
|
660
|
+
Jamf::Script.all_names(:refresh, cnx: jamf_cnx).include? jamf_uninstall_script_name
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Create or fetch the script that uninstalls this title from a Mac
|
|
664
|
+
#
|
|
665
|
+
# @return [Jamf::Script] The Jamf Script for uninstalling this title
|
|
666
|
+
#####################################
|
|
667
|
+
def jamf_uninstall_script
|
|
668
|
+
return @jamf_uninstall_script if @jamf_uninstall_script
|
|
669
|
+
|
|
670
|
+
if jamf_uninstall_script_exist?
|
|
671
|
+
@jamf_uninstall_script = Jamf::Script.fetch name: jamf_uninstall_script_name, cnx: jamf_cnx
|
|
672
|
+
else
|
|
673
|
+
return if deleting?
|
|
674
|
+
|
|
675
|
+
progress "Jamf: Creating Uninstall script '#{jamf_uninstall_script_name}'", log: :info
|
|
676
|
+
@jamf_uninstall_script = Jamf::Script.create(
|
|
677
|
+
name: jamf_uninstall_script_name,
|
|
678
|
+
cnx: jamf_cnx
|
|
679
|
+
)
|
|
680
|
+
@jamf_uninstall_script.save
|
|
681
|
+
end
|
|
682
|
+
@jamf_uninstall_script
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Configure the uninstall script in jamf with our uninstall_script contents
|
|
686
|
+
#
|
|
687
|
+
# @return [void]
|
|
688
|
+
################################
|
|
689
|
+
def configure_jamf_uninstall_script
|
|
690
|
+
# if we don't have an uninstall script, nothing to do, it will be deleted elsewhere
|
|
691
|
+
return unless uninstall_script_contents
|
|
692
|
+
|
|
693
|
+
progress "Jamf: Congfiguring the uninstall script '#{jamf_uninstall_script_name}'", log: :info
|
|
694
|
+
jamf_uninstall_script.code = uninstall_script_contents
|
|
695
|
+
jamf_uninstall_script.save
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# do we need to update the uninstall scriptin jamf?
|
|
699
|
+
# true if our incoming changes include :uninstall_script OR :uninstall_ids
|
|
700
|
+
# and the new value of at least one of them is not empty
|
|
701
|
+
#
|
|
702
|
+
# (in which case we'll delete it)
|
|
703
|
+
#
|
|
704
|
+
# @return [Boolean]
|
|
705
|
+
########################
|
|
706
|
+
def need_to_update_jamf_uninstall_script?
|
|
707
|
+
if changes_for_update.key?(:uninstall_script)
|
|
708
|
+
!changes_for_update[:uninstall_script][:new].pix_empty?
|
|
709
|
+
elsif changes_for_update.key?(:uninstall_ids)
|
|
710
|
+
!changes_for_update[:uninstall_ids][:new].pix_empty?
|
|
711
|
+
else
|
|
712
|
+
false
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
#########################
|
|
717
|
+
def delete_jamf_uninstall_script
|
|
718
|
+
return unless jamf_uninstall_script_exist?
|
|
719
|
+
|
|
720
|
+
progress "Jamf: Deleting uninstall script '#{jamf_uninstall_script_name}'", log: :info
|
|
721
|
+
jamf_uninstall_script.delete
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# repair the uninstall script and policy in jamf
|
|
725
|
+
#####################
|
|
726
|
+
def repair_jamf_uninstall_script
|
|
727
|
+
if uninstall_script_contents
|
|
728
|
+
configure_jamf_uninstall_script
|
|
729
|
+
else
|
|
730
|
+
delete_jamf_uninstall_script
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# do we need to delete the uninstall script stuff in jamf?
|
|
735
|
+
#
|
|
736
|
+
# @return [Boolean]
|
|
737
|
+
#############################
|
|
738
|
+
def need_to_delete_jamf_uninstall_script?
|
|
739
|
+
uninstall_script_contents.pix_empty?
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# @return [String] the URL for the uninstall script in Jamf Pro
|
|
743
|
+
######################
|
|
744
|
+
def jamf_uninstall_script_url
|
|
745
|
+
return @jamf_uninstall_script_url if @jamf_uninstall_script_url
|
|
746
|
+
return unless uninstallable?
|
|
747
|
+
|
|
748
|
+
scr_id = Jamf::Script.valid_id jamf_uninstall_script_name, cnx: jamf_cnx
|
|
749
|
+
return unless scr_id
|
|
750
|
+
|
|
751
|
+
@jamf_uninstall_script_url = "#{jamf_gui_url}/view/settings/computer-management/scripts/#{scr_id}?tab=script"
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
####### The Uninstall Policy-
|
|
755
|
+
###########################################
|
|
756
|
+
###########################################
|
|
757
|
+
|
|
758
|
+
# @return [Boolean] Does the jamf_uninstall_policy exist?
|
|
759
|
+
#########################
|
|
760
|
+
def jamf_uninstall_policy_exist?
|
|
761
|
+
Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_uninstall_policy_name
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Create or fetch the policy that runs the jamf uninstall script
|
|
765
|
+
#
|
|
766
|
+
# @return [Jamf::Policy] The Jamf Policy for uninstalling this title
|
|
767
|
+
#####################################
|
|
768
|
+
def jamf_uninstall_policy
|
|
769
|
+
return @jamf_uninstall_policy if @jamf_uninstall_policy
|
|
770
|
+
|
|
771
|
+
if jamf_uninstall_policy_exist?
|
|
772
|
+
@jamf_uninstall_policy = Jamf::Policy.fetch name: jamf_uninstall_policy_name, cnx: jamf_cnx
|
|
773
|
+
else
|
|
774
|
+
return if deleting?
|
|
775
|
+
|
|
776
|
+
progress "Jamf: Creating Uninstall policy: '#{jamf_uninstall_policy_name}'", log: :info
|
|
777
|
+
@jamf_uninstall_policy = Jamf::Policy.create name: jamf_uninstall_policy_name, cnx: jamf_cnx
|
|
778
|
+
configure_jamf_uninstall_policy(jamf_uninstall_policy)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
@jamf_uninstall_policy
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Configure the uninstall policy
|
|
785
|
+
# @param pol [Jamf::Policy] the policy to configure
|
|
786
|
+
############################
|
|
787
|
+
def configure_jamf_uninstall_policy(pol = nil)
|
|
788
|
+
progress "Jamf: Configuring uninstall policy '#{jamf_uninstall_policy_name}'", log: :info
|
|
789
|
+
pol ||= jamf_uninstall_policy
|
|
790
|
+
pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
|
|
791
|
+
pol.add_script jamf_uninstall_script_name
|
|
792
|
+
pol.set_trigger_event :checkin, false
|
|
793
|
+
pol.set_trigger_event :custom, jamf_uninstall_policy_name
|
|
794
|
+
pol.scope.add_target(:computer_group, jamf_installed_group_name)
|
|
795
|
+
pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name] if valid_forced_exclusion_group_name
|
|
796
|
+
pol.frequency = :ongoing
|
|
797
|
+
pol.enable
|
|
798
|
+
pol.save
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# repair the uninstall script and policy in jamf
|
|
802
|
+
#####################
|
|
803
|
+
def repair_jamf_uninstall_policy
|
|
804
|
+
if uninstall_script_contents
|
|
805
|
+
configure_jamf_uninstall_policy
|
|
806
|
+
else
|
|
807
|
+
delete_jamf_uninstall_policy
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# delete the policy first if it exists
|
|
812
|
+
|
|
813
|
+
#########################
|
|
814
|
+
def delete_jamf_uninstall_policy
|
|
815
|
+
return unless jamf_uninstall_policy_exist?
|
|
816
|
+
|
|
817
|
+
progress "Jamf: Deleting uninstall policy '#{jamf_uninstall_policy_name}'", log: :info
|
|
818
|
+
jamf_uninstall_policy.delete
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# @return [String] the URL for the uninstall policy in Jamf Pro
|
|
822
|
+
######################
|
|
823
|
+
def jamf_uninstall_policy_url
|
|
824
|
+
return @jamf_uninstall_policy_url if @jamf_uninstall_policy_url
|
|
825
|
+
return unless uninstallable?
|
|
826
|
+
|
|
827
|
+
pol_id = Jamf::Policy.valid_id jamf_uninstall_policy_name, cnx: jamf_cnx
|
|
828
|
+
return unless pol_id
|
|
829
|
+
|
|
830
|
+
@jamf_uninstall_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
####### The Installed Group
|
|
834
|
+
###########################################
|
|
835
|
+
###########################################
|
|
836
|
+
|
|
837
|
+
# @return [Boolean] Does the jamf_installed_group exist?
|
|
838
|
+
##########################
|
|
839
|
+
def jamf_installed_group_exist?
|
|
840
|
+
Jamf::ComputerGroup.all_names(:refresh, cnx: jamf_cnx).include? jamf_installed_group_name
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# Create or fetch he smartgroup in jamf that contains all macs
|
|
844
|
+
# with any version of this title installed.
|
|
845
|
+
# If we are deleting and it doesn't exist, return nil.
|
|
846
|
+
#
|
|
847
|
+
# @return [Jamf::ComputerGroup, nil] The Jamf ComputerGroup for this title's installed computers
|
|
848
|
+
#####################################
|
|
849
|
+
def jamf_installed_group
|
|
850
|
+
return @jamf_installed_group if @jamf_installed_group
|
|
851
|
+
|
|
852
|
+
if jamf_installed_group_exist?
|
|
853
|
+
@jamf_installed_group = Jamf::ComputerGroup.fetch(
|
|
854
|
+
name: jamf_installed_group_name,
|
|
855
|
+
cnx: jamf_cnx
|
|
856
|
+
)
|
|
857
|
+
else
|
|
858
|
+
return if deleting?
|
|
859
|
+
|
|
860
|
+
progress "Jamf: Creating smart group '#{jamf_installed_group_name}'", log: :info
|
|
861
|
+
|
|
862
|
+
@jamf_installed_group = Jamf::ComputerGroup.create(
|
|
863
|
+
name: jamf_installed_group_name,
|
|
864
|
+
type: :smart,
|
|
865
|
+
cnx: jamf_cnx
|
|
866
|
+
)
|
|
867
|
+
@jamf_installed_group.save
|
|
868
|
+
configure_jamf_installed_group
|
|
869
|
+
log_debug 'Jamf: Sleeping to let Jamf server see change to the Installed smart group.'
|
|
870
|
+
sleep 10
|
|
871
|
+
end
|
|
872
|
+
@jamf_installed_group
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# Set the configuration of jamf_installed_group
|
|
876
|
+
#
|
|
877
|
+
# @return [void]
|
|
878
|
+
#####################################
|
|
879
|
+
def configure_jamf_installed_group
|
|
880
|
+
progress "Jamf: Configuring smart group '#{jamf_installed_group_name}'", log: :info
|
|
881
|
+
|
|
882
|
+
jamf_installed_group.criteria = Jamf::Criteriable::Criteria.new(jamf_installed_group_criteria)
|
|
883
|
+
jamf_installed_group.save
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# The criteria for the smart group in Jamf that contains all Macs
|
|
887
|
+
# with any version of this title installed
|
|
888
|
+
#
|
|
889
|
+
# If we have, or are about to update to, a version_script (EA) then use it,
|
|
890
|
+
# otherwise use the app_name and app_bundle_id.
|
|
891
|
+
#
|
|
892
|
+
# @return [Array<Jamf::Criteriable::Criterion>]
|
|
893
|
+
###################################
|
|
894
|
+
def jamf_installed_group_criteria
|
|
895
|
+
have_vers_script =
|
|
896
|
+
if changes_for_update&.dig :version_script
|
|
897
|
+
changes_for_update[:version_script][:new]
|
|
898
|
+
else
|
|
899
|
+
version_script
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# If we have a version_script, use the ea
|
|
903
|
+
if have_vers_script
|
|
904
|
+
[
|
|
905
|
+
Jamf::Criteriable::Criterion.new(
|
|
906
|
+
and_or: :and,
|
|
907
|
+
name: jamf_normal_ea_name,
|
|
908
|
+
search_type: 'is not',
|
|
909
|
+
value: Xolo::BLANK
|
|
910
|
+
)
|
|
911
|
+
]
|
|
912
|
+
|
|
913
|
+
# No version script, so we must be using app data
|
|
914
|
+
else
|
|
915
|
+
aname = changes_for_update.dig(:app_name, :new) || app_name
|
|
916
|
+
abundle = changes_for_update.dig(:app_bundle_id, :new) || app_bundle_id
|
|
917
|
+
|
|
918
|
+
[
|
|
919
|
+
Jamf::Criteriable::Criterion.new(
|
|
920
|
+
and_or: :and,
|
|
921
|
+
name: 'Application Title',
|
|
922
|
+
search_type: 'is',
|
|
923
|
+
value: aname
|
|
924
|
+
),
|
|
925
|
+
|
|
926
|
+
Jamf::Criteriable::Criterion.new(
|
|
927
|
+
and_or: :and,
|
|
928
|
+
name: 'Application Bundle ID',
|
|
929
|
+
search_type: 'is',
|
|
930
|
+
value: abundle
|
|
931
|
+
)
|
|
932
|
+
]
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# do we need to update the 'installed' smart group?
|
|
937
|
+
# true if our incoming changes include the app_name or app_bundle_id
|
|
938
|
+
#
|
|
939
|
+
# If they changed at all, we need to update no matter what:
|
|
940
|
+
# - if they are now nil, we switched to a version script
|
|
941
|
+
#
|
|
942
|
+
# - if they aren't nil but are different, we need to update
|
|
943
|
+
# the group criteria to reflect that.
|
|
944
|
+
#
|
|
945
|
+
# Changes to the version script, if it was in use before, don't
|
|
946
|
+
# require us to change the smart group
|
|
947
|
+
#
|
|
948
|
+
#
|
|
949
|
+
# @return [Boolean]
|
|
950
|
+
#########################
|
|
951
|
+
def need_to_update_jamf_installed_group?
|
|
952
|
+
changes_for_update[:app_name] || changes_for_update[:app_bundle_id]
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
# Delete the 'installed' smart group
|
|
956
|
+
# @return [void]
|
|
957
|
+
######################################
|
|
958
|
+
def delete_jamf_installed_group
|
|
959
|
+
return unless jamf_installed_group_exist?
|
|
960
|
+
|
|
961
|
+
progress "Jamf: Deleting smart group '#{jamf_installed_group_name}'", log: :info
|
|
962
|
+
jamf_installed_group.delete
|
|
963
|
+
# give the server time to see the deletion
|
|
964
|
+
log_debug 'Sleeping to let server see deletion of smart group'
|
|
965
|
+
sleep 10
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# @return [String] the URL for the Frozen statig group in Jamf Pro
|
|
969
|
+
######################
|
|
970
|
+
def jamf_installed_group_url
|
|
971
|
+
return @jamf_installed_group_url if @jamf_installed_group_url
|
|
972
|
+
|
|
973
|
+
gr_id = Jamf::ComputerGroup.valid_id jamf_installed_group_name, cnx: jamf_cnx
|
|
974
|
+
return unless gr_id
|
|
975
|
+
|
|
976
|
+
@jamf_installed_group_url = "#{jamf_gui_url}/smartComputerGroups.html?id=#{gr_id}&o=r"
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
####### The Frozen Group
|
|
980
|
+
###########################################
|
|
981
|
+
###########################################
|
|
982
|
+
|
|
983
|
+
# @return [Boolean] Does the jamf_frozen_group exist?
|
|
984
|
+
###########################
|
|
985
|
+
def jamf_frozen_group_exist?
|
|
986
|
+
Jamf::ComputerGroup.all_names(:refresh, cnx: jamf_cnx).include? jamf_frozen_group_name
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
# Create or fetch static in jamf that contains macs with this title 'frozen'
|
|
990
|
+
# If we are deleting and it doesn't exist, return nil.
|
|
991
|
+
# There really isn't any configuration or repairing to do, it's just a static group.
|
|
992
|
+
#
|
|
993
|
+
# @return [Jamf::ComputerGroup, nil] The Jamf ComputerGroup for this title's frozen computers
|
|
994
|
+
#####################################
|
|
995
|
+
def jamf_frozen_group
|
|
996
|
+
return @jamf_frozen_group if @jamf_frozen_group
|
|
997
|
+
|
|
998
|
+
if jamf_frozen_group_exist?
|
|
999
|
+
@jamf_frozen_group = Jamf::ComputerGroup.fetch name: jamf_frozen_group_name, cnx: jamf_cnx
|
|
1000
|
+
else
|
|
1001
|
+
return if deleting?
|
|
1002
|
+
|
|
1003
|
+
progress "Jamf: Creating static group '#{jamf_frozen_group_name}' with no members at the moment", log: :info
|
|
1004
|
+
|
|
1005
|
+
@jamf_frozen_group = Jamf::ComputerGroup.create(
|
|
1006
|
+
name: jamf_frozen_group_name,
|
|
1007
|
+
type: :static,
|
|
1008
|
+
cnx: jamf_cnx
|
|
1009
|
+
)
|
|
1010
|
+
@jamf_frozen_group.save
|
|
1011
|
+
|
|
1012
|
+
end
|
|
1013
|
+
@jamf_frozen_group
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# Freeze or thaw an array of computers for a title
|
|
1017
|
+
#
|
|
1018
|
+
# @param action [Symbol] :freeze or :thaw
|
|
1019
|
+
#
|
|
1020
|
+
# @param computers [Array<String>, String] The computer name[s] to freeze or thaw. To thaw
|
|
1021
|
+
# all computers pass Xolo::TARGET_ALL (freeze all is not allowed)
|
|
1022
|
+
#
|
|
1023
|
+
# @return [Hash] Keys are computer names, values are Xolo::OK or an error message
|
|
1024
|
+
#################################
|
|
1025
|
+
def freeze_or_thaw_computers(action:, computers:)
|
|
1026
|
+
return unless %i[freeze thaw].include? action
|
|
1027
|
+
|
|
1028
|
+
# convert to an array if it's a single string
|
|
1029
|
+
computers = [computers].flatten
|
|
1030
|
+
|
|
1031
|
+
result, changes_to_log =
|
|
1032
|
+
if action == :thaw
|
|
1033
|
+
thaw_computers(computers: computers)
|
|
1034
|
+
else
|
|
1035
|
+
freeze_computers(computers: computers)
|
|
1036
|
+
end # if action ==
|
|
1037
|
+
|
|
1038
|
+
jamf_frozen_group.save
|
|
1039
|
+
|
|
1040
|
+
unless changes_to_log.empty?
|
|
1041
|
+
action_msg =
|
|
1042
|
+
if action == :freeze
|
|
1043
|
+
"Froze computers: #{changes_to_log.join(', ')}"
|
|
1044
|
+
else
|
|
1045
|
+
"Thawed computers: #{changes_to_log.join(', ')}"
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
log_change msg: action_msg
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
result
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
# freeze some computers
|
|
1055
|
+
# see #freeze_or_thaw_computers
|
|
1056
|
+
##############
|
|
1057
|
+
def freeze_computers(computers:)
|
|
1058
|
+
result = {}
|
|
1059
|
+
freezes_to_log = []
|
|
1060
|
+
|
|
1061
|
+
comp_names = Jamf::Computer.all_names cnx: jamf_cnx
|
|
1062
|
+
grp_members = jamf_frozen_group.member_names
|
|
1063
|
+
|
|
1064
|
+
computers.each do |comp|
|
|
1065
|
+
if grp_members.include? comp
|
|
1066
|
+
log_info "Not freezing computer '#{comp}' for title '#{title}', already frozen"
|
|
1067
|
+
result[comp] = "#{Xolo::ERROR}: Already frozen"
|
|
1068
|
+
elsif comp_names.include? comp
|
|
1069
|
+
log_info "Freezing computer '#{comp}' for title '#{title}'"
|
|
1070
|
+
jamf_frozen_group.add_member comp
|
|
1071
|
+
result[comp] = Xolo::OK
|
|
1072
|
+
freezes_to_log << comp
|
|
1073
|
+
else
|
|
1074
|
+
log_debug "Cannot freeze computer '#{comp}' for title '#{title}', no such computer"
|
|
1075
|
+
result[comp] = "#{Xolo::ERROR}: No computer with that name"
|
|
1076
|
+
end # if comp_names.include
|
|
1077
|
+
end # computers.each
|
|
1078
|
+
|
|
1079
|
+
[result, freezes_to_log]
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
# thaw some computers
|
|
1083
|
+
# see #freeze_or_thaw_computers
|
|
1084
|
+
##############
|
|
1085
|
+
def thaw_computers(computers:)
|
|
1086
|
+
result = {}
|
|
1087
|
+
thaws_to_log = []
|
|
1088
|
+
|
|
1089
|
+
if computers.include? Xolo::TARGET_ALL
|
|
1090
|
+
log_info "Thawing all computers for title: '#{title}'"
|
|
1091
|
+
jamf_frozen_group.clear
|
|
1092
|
+
result[Xolo::TARGET_ALL] = Xolo::OK
|
|
1093
|
+
thaws_to_log << Xolo::TARGET_ALL
|
|
1094
|
+
else
|
|
1095
|
+
|
|
1096
|
+
grp_members = jamf_frozen_group.member_names
|
|
1097
|
+
computers.each do |comp|
|
|
1098
|
+
if grp_members.include? comp
|
|
1099
|
+
jamf_frozen_group.remove_member comp
|
|
1100
|
+
log_info "Thawed computer '#{comp}' for title '#{title}'"
|
|
1101
|
+
result[comp] = Xolo::OK
|
|
1102
|
+
thaws_to_log << comp
|
|
1103
|
+
else
|
|
1104
|
+
log_debug "Cannot thaw computer '#{comp}' for title '#{title}', not frozen"
|
|
1105
|
+
result[comp] = "#{Xolo::ERROR}: Not frozen"
|
|
1106
|
+
end # if grp_members.include? comp
|
|
1107
|
+
end # computers.each
|
|
1108
|
+
end # if computers.include?
|
|
1109
|
+
|
|
1110
|
+
[result, thaws_to_log]
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Return the members of the 'frozen' static group for a title
|
|
1114
|
+
#
|
|
1115
|
+
# @return [Hash{String => String}] computer name => user name
|
|
1116
|
+
#################################
|
|
1117
|
+
def frozen_computers
|
|
1118
|
+
members = {}
|
|
1119
|
+
|
|
1120
|
+
comps = jamf_frozen_group.member_names
|
|
1121
|
+
comps_to_users = Jamf::Computer.map_all :name, to: :username, cnx: jamf_cnx
|
|
1122
|
+
|
|
1123
|
+
comps.each { |comp| members[comp] = comps_to_users[comp] || 'unknown' }
|
|
1124
|
+
|
|
1125
|
+
members
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
# repair the frozen group
|
|
1129
|
+
###################
|
|
1130
|
+
def repair_frozen_group
|
|
1131
|
+
progress 'Jamf: Ensuring frozen static group exists', log: :debug
|
|
1132
|
+
# This creates it if it doesn't exist. Nothing more we can do here.
|
|
1133
|
+
jamf_frozen_group
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Delete the 'frozen' static group
|
|
1137
|
+
# @return [void]
|
|
1138
|
+
######################################
|
|
1139
|
+
def delete_jamf_frozen_group
|
|
1140
|
+
grp_id = Jamf::ComputerGroup.valid_id jamf_frozen_group_name, cnx: jamf_cnx
|
|
1141
|
+
return unless grp_id
|
|
1142
|
+
|
|
1143
|
+
progress "Jamf: Deleting static group '#{jamf_frozen_group_name}'", log: :info
|
|
1144
|
+
Jamf::ComputerGroup.delete grp_id, cnx: jamf_cnx
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
# @return [String] the URL for the Frozen static group in Jamf Pro
|
|
1148
|
+
######################
|
|
1149
|
+
def jamf_frozen_group_url
|
|
1150
|
+
return @jamf_frozen_group_url if @jamf_frozen_group_url
|
|
1151
|
+
|
|
1152
|
+
gr_id = Jamf::ComputerGroup.valid_id jamf_frozen_group_name, cnx: jamf_cnx
|
|
1153
|
+
return unless gr_id
|
|
1154
|
+
|
|
1155
|
+
@jamf_frozen_group_url = "#{jamf_gui_url}/staticComputerGroups.html?id=#{gr_id}&o=r"
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
####### The Expire Policy
|
|
1159
|
+
###########################################
|
|
1160
|
+
###########################################
|
|
1161
|
+
|
|
1162
|
+
# @return [Boolean] Does the jamf_expire_policy exist?
|
|
1163
|
+
#########################
|
|
1164
|
+
def jamf_expire_policy_exist?
|
|
1165
|
+
Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_expire_policy_name
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
# Create or fetch the policy that expires a title
|
|
1169
|
+
#
|
|
1170
|
+
# @return [Jamf::Policy] The Jamf Policy for expiring this title
|
|
1171
|
+
#####################################
|
|
1172
|
+
def jamf_expire_policy
|
|
1173
|
+
return @jamf_expire_policy if @jamf_expire_policy
|
|
1174
|
+
|
|
1175
|
+
if jamf_expire_policy_exist?
|
|
1176
|
+
@jamf_expire_policy = Jamf::Policy.fetch name: jamf_expire_policy_name, cnx: jamf_cnx
|
|
1177
|
+
else
|
|
1178
|
+
return if deleting?
|
|
1179
|
+
|
|
1180
|
+
progress "Jamf: Creating Expiration policy: '#{jamf_expire_policy_name}'", log: :info
|
|
1181
|
+
|
|
1182
|
+
@jamf_expire_policy = Jamf::Policy.create name: jamf_expire_policy_name, cnx: jamf_cnx
|
|
1183
|
+
configure_jamf_expire_policy
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
@jamf_expire_policy
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
# Configure the expiration policy
|
|
1190
|
+
# @param pol [Jamf::Policy] the policy to configure
|
|
1191
|
+
#########################
|
|
1192
|
+
def configure_jamf_expire_policy
|
|
1193
|
+
pol = jamf_expire_policy
|
|
1194
|
+
pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
|
|
1195
|
+
pol.run_command = "#{Xolo::Server::Title::CLIENT_EXPIRE_COMMAND} #{title}"
|
|
1196
|
+
pol.set_trigger_event :checkin, true
|
|
1197
|
+
pol.set_trigger_event :custom, jamf_expire_policy_name
|
|
1198
|
+
pol.scope.add_target(:computer_group, jamf_installed_group_name)
|
|
1199
|
+
pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name] if valid_forced_exclusion_group_name
|
|
1200
|
+
pol.frequency = :daily
|
|
1201
|
+
pol.enable
|
|
1202
|
+
pol.save
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
#############################
|
|
1206
|
+
def delete_jamf_expire_policy
|
|
1207
|
+
return unless jamf_expire_policy_exist?
|
|
1208
|
+
|
|
1209
|
+
progress "Jamf: Deleting expiration policy '#{jamf_expire_policy_name}'", log: :info
|
|
1210
|
+
jamf_expire_policy.delete
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
# repair the expire policy in jamf
|
|
1214
|
+
#####################
|
|
1215
|
+
def repair_jamf_expire_policy
|
|
1216
|
+
if expiration && !expire_paths.pix_empty?
|
|
1217
|
+
progress "Jamf: Repairing expiration policy '#{jamf_expire_policy_name}'"
|
|
1218
|
+
configure_jamf_expire_policy
|
|
1219
|
+
|
|
1220
|
+
else
|
|
1221
|
+
delete_jamf_expire_policy
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
# @return [String] the URL for the uninstall policy in Jamf Pro
|
|
1226
|
+
######################
|
|
1227
|
+
def jamf_expire_policy_url
|
|
1228
|
+
return @jamf_expire_policy_url if @jamf_expire_policy_url
|
|
1229
|
+
return unless uninstallable?
|
|
1230
|
+
return unless expiration
|
|
1231
|
+
|
|
1232
|
+
pol_id = Jamf::Policy.valid_id jamf_expire_policy_name, cnx: jamf_cnx
|
|
1233
|
+
return unless pol_id
|
|
1234
|
+
|
|
1235
|
+
@jamf_expire_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
####### The Patch Title
|
|
1239
|
+
###########################################
|
|
1240
|
+
###########################################
|
|
1241
|
+
|
|
1242
|
+
# The Jamf Patch Source that is connected to the Title Editor
|
|
1243
|
+
# This must be manually configured in the Jamf server and the Xolo server
|
|
1244
|
+
#
|
|
1245
|
+
# @return [Jamf::PatchSource] The Jamf Patch Source
|
|
1246
|
+
#########################
|
|
1247
|
+
def jamf_ted_patch_source
|
|
1248
|
+
@jamf_ted_patch_source ||=
|
|
1249
|
+
Jamf::PatchSource.fetch(name: Xolo::Server.config.ted_patch_source, cnx: jamf_cnx)
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
# The titles available from the Title Editor via its
|
|
1253
|
+
# Jamf Patch Source. These are titles have have been enabled
|
|
1254
|
+
# in the Title Editor
|
|
1255
|
+
#
|
|
1256
|
+
# available_titles returns a Hash for each available title, with these keys:
|
|
1257
|
+
#
|
|
1258
|
+
# name_id: [String] The Xolo 'title' or the Title Editor 'id'
|
|
1259
|
+
#
|
|
1260
|
+
# current_version: [String] NOTE: This
|
|
1261
|
+
# may be a version that is in 'pilot' from Xolo's POV, but
|
|
1262
|
+
# from the TEd's POV, it has been made available to Jamf.
|
|
1263
|
+
#
|
|
1264
|
+
# publisher: [String]
|
|
1265
|
+
#
|
|
1266
|
+
# last_modified: [Time]
|
|
1267
|
+
#
|
|
1268
|
+
# app_name: [String] The Xolo 'display_name'
|
|
1269
|
+
#
|
|
1270
|
+
# but we map it to just the name_id
|
|
1271
|
+
#
|
|
1272
|
+
# @return [Array<String>] info about the available titles
|
|
1273
|
+
#########################
|
|
1274
|
+
def jamf_ted_available_titles
|
|
1275
|
+
# Don't cache this in an instance var, it changes during the
|
|
1276
|
+
# life of our title instance
|
|
1277
|
+
# jamf_ted_patch_source.available_titles.map { |t| t[:name_id] }
|
|
1278
|
+
# Also NOTE: "available" means not only enabled
|
|
1279
|
+
# in the title editor, but also not already active in jamf.
|
|
1280
|
+
# So any given title will either be here or in
|
|
1281
|
+
# jamf_active_ted_titles, but never both.
|
|
1282
|
+
jamf_ted_patch_source.available_name_ids
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
# @return [Boolean] Is this xolo title available in Jamf?
|
|
1286
|
+
########################
|
|
1287
|
+
def jamf_ted_title_available?
|
|
1288
|
+
jamf_ted_available_titles.include? title
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
# The titles active in Jamf Patch Management from the Title Editor
|
|
1292
|
+
# This takes into account that other Patch Sources may have titles with the
|
|
1293
|
+
# same 'name_id' (the xolo 'title')
|
|
1294
|
+
# A hash keyed by the title, with values of the jamf title id
|
|
1295
|
+
#
|
|
1296
|
+
# @return [Hash {String => Integer}] The xolo titles that are active in Jamf Patch Management
|
|
1297
|
+
########################
|
|
1298
|
+
def jamf_active_ted_titles(refresh: false)
|
|
1299
|
+
@jamf_active_ted_titles = nil if refresh
|
|
1300
|
+
return @jamf_active_ted_titles if @jamf_active_ted_titles
|
|
1301
|
+
|
|
1302
|
+
@jamf_active_ted_titles = {}
|
|
1303
|
+
active_from_ted = Jamf::PatchTitle.all(:refresh, cnx: jamf_cnx).select do |t|
|
|
1304
|
+
t[:source_id] == jamf_ted_patch_source.id
|
|
1305
|
+
end
|
|
1306
|
+
active_from_ted.each { |t| @jamf_active_ted_titles[t[:name_id]] = t[:id] }
|
|
1307
|
+
@jamf_active_ted_titles
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
# @return [Boolean] Is this xolo title currently active in Jamf?
|
|
1311
|
+
########################
|
|
1312
|
+
def jamf_ted_title_active?
|
|
1313
|
+
jamf_active_ted_titles(refresh: true).key? title
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
# The Jamf ID of the Patch Title for this xolo title
|
|
1317
|
+
# if it has been activated in jamf.
|
|
1318
|
+
#
|
|
1319
|
+
# @return [Integer, nil] The Jamf ID of this title, if it is active in Jamf
|
|
1320
|
+
########################
|
|
1321
|
+
def jamf_patch_title_id
|
|
1322
|
+
@jamf_patch_title_id ||= jamf_active_ted_titles(refresh: true)[title]
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# create/activate the patch title in Jamf Pro, if not already done.
|
|
1326
|
+
#
|
|
1327
|
+
# This 'subscribes' Jamf to the title in the title editor
|
|
1328
|
+
# It must be enabled in the Title Editor first, meaning
|
|
1329
|
+
# it has at least one requirement, and at least one enabled patch/version.
|
|
1330
|
+
#
|
|
1331
|
+
# The 'stub' patch version should allow this when we create the title.
|
|
1332
|
+
#
|
|
1333
|
+
# Xolo should have enabled it in the Title editor before we
|
|
1334
|
+
# reach this point.
|
|
1335
|
+
#
|
|
1336
|
+
##########################
|
|
1337
|
+
def activate_jamf_patch_title
|
|
1338
|
+
if jamf_ted_title_active?
|
|
1339
|
+
log_debug "Jamf: Title '#{display_name}' (#{title}) is already active in Jamf"
|
|
1340
|
+
return
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# wait up to 60secs for the title to become available
|
|
1344
|
+
counter = 0
|
|
1345
|
+
until jamf_ted_title_available? || counter == 12
|
|
1346
|
+
log_debug "Jamf: Waiting for title '#{display_name}' (#{title}) to become available from the Title Editor"
|
|
1347
|
+
sleep 5
|
|
1348
|
+
counter += 1
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
unless jamf_ted_title_available?
|
|
1352
|
+
msg = "Jamf: Title '#{title}' is not yet available to Jamf. Make sure it has at least one version enabled in the Title Editor"
|
|
1353
|
+
log_error msg
|
|
1354
|
+
raise Xolo::NoSuchItemError, msg
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
# This creates/activates the title if needed
|
|
1358
|
+
jamf_patch_title
|
|
1359
|
+
|
|
1360
|
+
accept_jamf_patch_ea
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
# Create or fetch the patch title object for this xolo title.
|
|
1364
|
+
# If we are deleting and it doesn't exist, return nil.
|
|
1365
|
+
#
|
|
1366
|
+
# @param refresh [Boolean] re-fetch the patch title from Jamf?
|
|
1367
|
+
# @return [Jamf::PatchTitle, nil] The Jamf Patch Title for this Xolo Title
|
|
1368
|
+
########################
|
|
1369
|
+
def jamf_patch_title(refresh: false)
|
|
1370
|
+
@jamf_patch_title = nil if refresh
|
|
1371
|
+
return @jamf_patch_title if @jamf_patch_title
|
|
1372
|
+
|
|
1373
|
+
breaktime = Time.now + Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR
|
|
1374
|
+
until jamf_ted_title_available? || jamf_ted_title_active?
|
|
1375
|
+
if Time.now > breaktime
|
|
1376
|
+
raise Xolo::ServerError,
|
|
1377
|
+
"Jamf: Title '#{title}' is not available in Jamf after waiting #{Xolo::Server::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} seconds"
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
log_debug "Waiting a long time for title '#{title}' to become available from TEd. Checking every 10 secs"
|
|
1381
|
+
sleep 10
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
if jamf_ted_title_active?
|
|
1385
|
+
@jamf_patch_title = Jamf::PatchTitle.fetch(id: jamf_patch_title_id, cnx: jamf_cnx)
|
|
1386
|
+
|
|
1387
|
+
else
|
|
1388
|
+
return if deleting?
|
|
1389
|
+
|
|
1390
|
+
msg = "Jamf: Activating Patch Title '#{display_name}' (#{title}) from the Title Editor Patch Source '#{Xolo::Server.config.ted_patch_source}'"
|
|
1391
|
+
progress msg, log: :info
|
|
1392
|
+
|
|
1393
|
+
@jamf_patch_title =
|
|
1394
|
+
Jamf::PatchTitle.create(
|
|
1395
|
+
name: display_name,
|
|
1396
|
+
source: Xolo::Server.config.ted_patch_source,
|
|
1397
|
+
name_id: title,
|
|
1398
|
+
cnx: jamf_cnx
|
|
1399
|
+
)
|
|
1400
|
+
@jamf_patch_title.category = Xolo::Server::JAMF_XOLO_CATEGORY
|
|
1401
|
+
@jamf_patch_title.save
|
|
1402
|
+
|
|
1403
|
+
end
|
|
1404
|
+
@jamf_patch_title
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
# Delete the patch title
|
|
1408
|
+
# NOTE: jamf api user must have 'delete computer ext. attribs' permmissions
|
|
1409
|
+
##############################
|
|
1410
|
+
def delete_jamf_patch_title
|
|
1411
|
+
return unless jamf_ted_title_active?
|
|
1412
|
+
|
|
1413
|
+
pt_id = Jamf::PatchTitle.map_all(:id, to: :name_id, cnx: jamf_cnx).invert[title]
|
|
1414
|
+
return unless pt_id
|
|
1415
|
+
|
|
1416
|
+
msg = "Jamf: Deleting (unsubscribing) title '#{display_name}' (#{title}}) in Jamf Patch Management"
|
|
1417
|
+
progress msg, log: :info
|
|
1418
|
+
Jamf::PatchTitle.delete pt_id, cnx: jamf_cnx
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
# @return [String] the URL for the Patch Title in Jamf Pro
|
|
1422
|
+
#####################
|
|
1423
|
+
def jamf_patch_title_url
|
|
1424
|
+
@jamf_patch_title_url ||= "#{jamf_gui_url}/view/computers/patch/#{jamf_patch_title_id}"
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
#### The Manual/SelfService Policy for the currently released version
|
|
1428
|
+
#####################################
|
|
1429
|
+
#####################################
|
|
1430
|
+
|
|
1431
|
+
# @return [Boolean] Does the jamf_manual_install_released_policy exist?
|
|
1432
|
+
#######################
|
|
1433
|
+
def jamf_manual_install_released_policy_exist?
|
|
1434
|
+
Jamf::Policy.all_names(:refresh, cnx: jamf_cnx).include? jamf_manual_install_released_policy_name
|
|
1435
|
+
end
|
|
1436
|
+
|
|
1437
|
+
# Create or fetch the manual install policy for the currently released version.
|
|
1438
|
+
# If we are deleting and it doesn't exist, return nil.
|
|
1439
|
+
# Also return nil if we have no version released
|
|
1440
|
+
#
|
|
1441
|
+
# @return [Jamf::Policy] The manual-install-policy for this version, if it exists
|
|
1442
|
+
##########################
|
|
1443
|
+
def jamf_manual_install_released_policy
|
|
1444
|
+
@jamf_manual_install_released_policy ||=
|
|
1445
|
+
if jamf_manual_install_released_policy_exist?
|
|
1446
|
+
Jamf::Policy.fetch(name: jamf_manual_install_released_policy_name, cnx: jamf_cnx)
|
|
1447
|
+
else
|
|
1448
|
+
return if deleting?
|
|
1449
|
+
|
|
1450
|
+
create_jamf_manual_install_released_policy
|
|
1451
|
+
end
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
# @return [String] the URL for the manual install policy in Jamf Pro
|
|
1455
|
+
######################
|
|
1456
|
+
def jamf_manual_install_released_policy_url
|
|
1457
|
+
return @jamf_manual_install_released_policy_url if @jamf_manual_install_released_policy_url
|
|
1458
|
+
|
|
1459
|
+
pol_id = Jamf::Policy.valid_id jamf_manual_install_released_policy_name, cnx: jamf_cnx
|
|
1460
|
+
return unless pol_id
|
|
1461
|
+
|
|
1462
|
+
@jamf_manual_install_released_policy_url = "#{jamf_gui_url}/policies.html?id=#{pol_id}&o=r"
|
|
1463
|
+
end
|
|
1464
|
+
|
|
1465
|
+
# The manual install policy for the current release is always scoped to all
|
|
1466
|
+
# computers, with exclusions
|
|
1467
|
+
#
|
|
1468
|
+
# The policy has a custom trigger, or can be installed via self service if
|
|
1469
|
+
# desired
|
|
1470
|
+
#
|
|
1471
|
+
#########################
|
|
1472
|
+
def create_jamf_manual_install_released_policy
|
|
1473
|
+
msg = "Jamf: Creating manual install policy for current release: '#{jamf_manual_install_released_policy_name}'"
|
|
1474
|
+
progress msg, log: :info
|
|
1475
|
+
|
|
1476
|
+
pol = Jamf::Policy.create name: jamf_manual_install_released_policy_name, cnx: jamf_cnx
|
|
1477
|
+
|
|
1478
|
+
configure_jamf_manual_install_released_policy(pol)
|
|
1479
|
+
pol.save
|
|
1480
|
+
pol
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
# Configure the settings for the manual_install_released_policy
|
|
1484
|
+
# @param pol [Jamf::Policy] the policy we are configuring
|
|
1485
|
+
# @return [void]
|
|
1486
|
+
###################
|
|
1487
|
+
def configure_jamf_manual_install_released_policy(pol)
|
|
1488
|
+
pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
|
|
1489
|
+
pol.set_trigger_event :checkin, false
|
|
1490
|
+
pol.set_trigger_event :custom, jamf_manual_install_released_policy_name
|
|
1491
|
+
pol.frequency = :ongoing
|
|
1492
|
+
pol.recon = false
|
|
1493
|
+
pol.scope.set_all_targets
|
|
1494
|
+
|
|
1495
|
+
# clear any existing packages
|
|
1496
|
+
pol.package_names.each { |pkg_name| pol.remove_package pkg_name }
|
|
1497
|
+
|
|
1498
|
+
# don't add a package or enable the pol if there's no released version
|
|
1499
|
+
desired_vers = releasing_version || released_version
|
|
1500
|
+
if desired_vers
|
|
1501
|
+
rel_vers = version_object(desired_vers)
|
|
1502
|
+
pol.add_package rel_vers.jamf_pkg_id
|
|
1503
|
+
pol.enable
|
|
1504
|
+
else
|
|
1505
|
+
pol.disable
|
|
1506
|
+
end
|
|
1507
|
+
|
|
1508
|
+
# figure out the exclusions.
|
|
1509
|
+
#
|
|
1510
|
+
# explicit exlusions for the title
|
|
1511
|
+
excls = changes_for_update&.key?(:excluded_groups) ? changes_for_update[:excluded_groups][:new].dup : excluded_groups.dup
|
|
1512
|
+
excls ||= []
|
|
1513
|
+
# plus the frozen group
|
|
1514
|
+
excls << jamf_frozen_group_name
|
|
1515
|
+
# plus any forced group from the server config
|
|
1516
|
+
excls << valid_forced_exclusion_group_name if valid_forced_exclusion_group_name
|
|
1517
|
+
# NOTE: we do not exclude existing installs, so that manual re-installs can be a thing.
|
|
1518
|
+
log_debug "Setting exclusions for manual install policy for current release: #{excls}"
|
|
1519
|
+
|
|
1520
|
+
pol.scope.set_exclusions :computer_groups, excls
|
|
1521
|
+
|
|
1522
|
+
return unless self_service
|
|
1523
|
+
|
|
1524
|
+
if pol.in_self_service?
|
|
1525
|
+
configure_pol_for_self_service(pol)
|
|
1526
|
+
else
|
|
1527
|
+
add_title_to_self_service(pol)
|
|
1528
|
+
end
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
# repair the jamf_manual_install_released_policy - the
|
|
1532
|
+
# policy that installs whatever is the current release
|
|
1533
|
+
#############################
|
|
1534
|
+
def repair_jamf_manual_install_released_policy
|
|
1535
|
+
return unless released_version
|
|
1536
|
+
|
|
1537
|
+
progress 'Jamf: Repairing the manual/Self Service install policy for the current release'
|
|
1538
|
+
pol = jamf_manual_install_released_policy
|
|
1539
|
+
configure_jamf_manual_install_released_policy(pol)
|
|
1540
|
+
pol.save
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
#############################
|
|
1544
|
+
def delete_jamf_manual_install_released_policy
|
|
1545
|
+
return unless jamf_manual_install_released_policy_exist?
|
|
1546
|
+
|
|
1547
|
+
msg = "Jamf: Deleting the manual/Self Service install policy for the current release '#{jamf_manual_install_released_policy_name}'"
|
|
1548
|
+
progress msg, log: :info
|
|
1549
|
+
jamf_manual_install_released_policy.delete
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
# Add the jamf_manual_install_released_policy to self service if needed
|
|
1553
|
+
# @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
|
|
1554
|
+
# @return [void]
|
|
1555
|
+
##################################
|
|
1556
|
+
def add_title_to_self_service(pol = nil)
|
|
1557
|
+
return unless self_service
|
|
1558
|
+
|
|
1559
|
+
pol ||= jamf_manual_install_released_policy
|
|
1560
|
+
|
|
1561
|
+
msg = "Jamf: Adding Manual Install Policy '#{pol.name}' to Self Service."
|
|
1562
|
+
progress msg, log: :info
|
|
1563
|
+
|
|
1564
|
+
pol.add_to_self_service
|
|
1565
|
+
configure_pol_for_self_service(pol)
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
# Update whether or not we are in self service, based on the setting in the title
|
|
1569
|
+
#
|
|
1570
|
+
#########################
|
|
1571
|
+
def update_ssvc
|
|
1572
|
+
return unless changes_for_update.key? :self_service
|
|
1573
|
+
|
|
1574
|
+
# Update the manual install policy
|
|
1575
|
+
pol = jamf_manual_install_released_policy
|
|
1576
|
+
return unless pol
|
|
1577
|
+
|
|
1578
|
+
# we should be in SSvc - changes_for_update[:self_service][:new] is a boolean
|
|
1579
|
+
if changes_for_update[:self_service][:new]
|
|
1580
|
+
add_title_to_self_service(pol) unless pol.in_self_service?
|
|
1581
|
+
|
|
1582
|
+
# we should not be in SSvc
|
|
1583
|
+
elsif pol.in_self_service?
|
|
1584
|
+
msg = "Jamf: Removing Manual Install Policy '#{pol.name}' from Self Service."
|
|
1585
|
+
progress msg, log: :info
|
|
1586
|
+
pol.remove_from_self_service
|
|
1587
|
+
end
|
|
1588
|
+
pol.save
|
|
1589
|
+
|
|
1590
|
+
# TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1593
|
+
# Update our self service category, based on the setting in the title
|
|
1594
|
+
# TODO: Allow multiple categories, and 'featuring' ?
|
|
1595
|
+
#
|
|
1596
|
+
#########################
|
|
1597
|
+
def update_ssvc_category
|
|
1598
|
+
return unless changes_for_update.key? :self_service_category
|
|
1599
|
+
|
|
1600
|
+
pol = jamf_manual_install_released_policy
|
|
1601
|
+
return unless pol
|
|
1602
|
+
|
|
1603
|
+
new_cat = changes_for_update[:self_service_category][:new]
|
|
1604
|
+
|
|
1605
|
+
progress(
|
|
1606
|
+
"Jamf: Updating Self Service Category to '#{new_cat}' for Manual Install Policy '#{pol.name}'.",
|
|
1607
|
+
log: :info
|
|
1608
|
+
)
|
|
1609
|
+
|
|
1610
|
+
old_cats = pol.self_service_categories.map { |c| c[:name] }
|
|
1611
|
+
old_cats.each { |c| pol.remove_self_service_category c }
|
|
1612
|
+
pol.add_self_service_category new_cat
|
|
1613
|
+
pol.save
|
|
1614
|
+
|
|
1615
|
+
# TODO: if we decide to use ssvc in patch policies, loop thru versions to make any changes
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
# Update the SSvc Icon for the policies used by this version
|
|
1619
|
+
#
|
|
1620
|
+
# @param ttl_obj [Xolo::Server::Title] The pre-instantiated title for ths version.
|
|
1621
|
+
# if nil, we'll instantiate it now
|
|
1622
|
+
#
|
|
1623
|
+
# @return [void]
|
|
1624
|
+
###############################
|
|
1625
|
+
def update_ssvc_icon(ttl_obj: nil)
|
|
1626
|
+
ttl_obj ||= title_object
|
|
1627
|
+
# update manual install policy
|
|
1628
|
+
|
|
1629
|
+
log_debug "Jamf: Updating SSvc Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'"
|
|
1630
|
+
pol = jamf_manual_install_policy
|
|
1631
|
+
return unless pol
|
|
1632
|
+
|
|
1633
|
+
pol.upload :icon, ttl_obj.ssvc_icon_file
|
|
1634
|
+
progress "Jamf: Updated Icon for Manual Install Policy '#{jamf_manual_install_policy_name}'",
|
|
1635
|
+
log: :debug
|
|
1636
|
+
|
|
1637
|
+
# TODO: When we figure out if we want patch policies to use
|
|
1638
|
+
# ssvc - they will need to be updated also
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
# configure the self-service settings of the manual_install_released_policy
|
|
1642
|
+
# @param pol [Jamf::Policy] The jamf_manual_install_released_policy, which may not be saved yet.
|
|
1643
|
+
# @return [void]
|
|
1644
|
+
############################
|
|
1645
|
+
def configure_pol_for_self_service(pol = nil)
|
|
1646
|
+
pol ||= jamf_manual_install_released_policy
|
|
1647
|
+
|
|
1648
|
+
# clear existing categories, re-add correct one
|
|
1649
|
+
pol.self_service_categories.each { |cat| pol.remove_self_service_category cat }
|
|
1650
|
+
pol.add_self_service_category self_service_category
|
|
1651
|
+
|
|
1652
|
+
pol.self_service_description = description
|
|
1653
|
+
pol.self_service_display_name = display_name
|
|
1654
|
+
pol.self_service_install_button_text = Xolo::Server::Title::SELF_SERVICE_INSTALL_BTN_TEXT
|
|
1655
|
+
return unless ssvc_icon_file
|
|
1656
|
+
|
|
1657
|
+
pol.save # won't do anything unless needed, but has to exist before we can upload icons
|
|
1658
|
+
pol.upload :icon, ssvc_icon_file
|
|
1659
|
+
self.ssvc_icon_id = Jamf::Policy.fetch(id: pol.id, cnx: jamf_cnx).icon.id
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
end # TitleJamfPro
|
|
1663
|
+
|
|
1664
|
+
end # Mixins
|
|
1665
|
+
|
|
1666
|
+
end # Server
|
|
1667
|
+
|
|
1668
|
+
end # Xolo
|