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,1143 @@
|
|
|
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
|
+
# frozen_string_literal: true
|
|
7
|
+
|
|
8
|
+
# main module
|
|
9
|
+
module Xolo
|
|
10
|
+
|
|
11
|
+
module Server
|
|
12
|
+
|
|
13
|
+
# A Title in Xolo, as used on the server
|
|
14
|
+
#
|
|
15
|
+
# The code in this file mostly deals with the data on the Xolo server itself, and
|
|
16
|
+
# general methods for manipulating the title.
|
|
17
|
+
#
|
|
18
|
+
# Code for interacting with the Title Editor and Jamf Pro are in the helpers and mixins.
|
|
19
|
+
#
|
|
20
|
+
# NOTE be sure to only instantiate these using the
|
|
21
|
+
# servers 'instantiate_title' method, or else
|
|
22
|
+
# they might not have all the correct innards
|
|
23
|
+
#
|
|
24
|
+
class Title < Xolo::Core::BaseClasses::Title
|
|
25
|
+
|
|
26
|
+
# Mixins
|
|
27
|
+
#############################
|
|
28
|
+
#############################
|
|
29
|
+
|
|
30
|
+
include Xolo::Server::Helpers::JamfPro
|
|
31
|
+
include Xolo::Server::Helpers::TitleEditor
|
|
32
|
+
include Xolo::Server::Helpers::Log
|
|
33
|
+
include Xolo::Server::Helpers::Notification
|
|
34
|
+
|
|
35
|
+
include Xolo::Server::Mixins::Changelog
|
|
36
|
+
include Xolo::Server::Mixins::TitleJamfAccess
|
|
37
|
+
include Xolo::Server::Mixins::TitleTedAccess
|
|
38
|
+
|
|
39
|
+
# Constants
|
|
40
|
+
######################
|
|
41
|
+
######################
|
|
42
|
+
|
|
43
|
+
# On the server, xolo titles are represented by directories
|
|
44
|
+
# in this directory, named with the title name.
|
|
45
|
+
#
|
|
46
|
+
# So a title 'foobar' would have a directory
|
|
47
|
+
# (Xolo::Server::DATA_DIR)/titles/foobar/
|
|
48
|
+
# and in there will be a file
|
|
49
|
+
# foobar.json
|
|
50
|
+
# with the data for the Title instance itself
|
|
51
|
+
#
|
|
52
|
+
# Also in there will be a 'versions' dir containing json
|
|
53
|
+
# files for each version of the title.
|
|
54
|
+
# See {Xolo::Server::Version}
|
|
55
|
+
#
|
|
56
|
+
TITLES_DIR = Xolo::Server::DATA_DIR + 'titles'
|
|
57
|
+
|
|
58
|
+
# when creating new titles in the title editor,
|
|
59
|
+
# This is the 'currentVersion', which is required
|
|
60
|
+
# when creating.
|
|
61
|
+
# When the first version/patch is added, the
|
|
62
|
+
# title's value for this will be updated.
|
|
63
|
+
NEW_TITLE_CURRENT_VERSION = '0.0.0'
|
|
64
|
+
|
|
65
|
+
# If a title has a 'version_script'
|
|
66
|
+
# the the contents are stored in the title dir
|
|
67
|
+
# in a file with this name
|
|
68
|
+
VERSION_SCRIPT_FILENAME = 'version-script'
|
|
69
|
+
|
|
70
|
+
# If a title is uninstallable, it will
|
|
71
|
+
# have a script in Jamf, which is also saved in this file
|
|
72
|
+
# on the xolo server.
|
|
73
|
+
UNINSTALL_SCRIPT_FILENAME = 'uninstall-script'
|
|
74
|
+
|
|
75
|
+
# In the TitleEditor, the version script is
|
|
76
|
+
# stored as an Extension Attribute - each title can
|
|
77
|
+
# only have one.
|
|
78
|
+
# and it needs a 'key', which is the name used to indicate the
|
|
79
|
+
# EA in various criteria, and is the EA name in Jamf Patch.
|
|
80
|
+
# The key is this value as a prefix on the title
|
|
81
|
+
# so for title 'foobar', it is 'xolo-foobar'
|
|
82
|
+
# That value is also used as the display name
|
|
83
|
+
TITLE_EDITOR_EA_KEY_PREFIX = Xolo::Server::JAMF_OBJECT_NAME_PFX
|
|
84
|
+
|
|
85
|
+
# The EA from the title editor, which is used in Jamf Patch
|
|
86
|
+
# cannot, unfortunately, be used as a criterion in normal
|
|
87
|
+
# smart groups or advanced searches.
|
|
88
|
+
# Since we need a smart group containing all macs with any
|
|
89
|
+
# version of the title installed, we need a second copy of the
|
|
90
|
+
# EA as a 'normal' EA.
|
|
91
|
+
#
|
|
92
|
+
# (That group is used as an exclusion to any auto-install initial-
|
|
93
|
+
# install policies, so that those policies don't stomp on the matching
|
|
94
|
+
# Patch Policies)
|
|
95
|
+
#
|
|
96
|
+
# The 'duplicate' EA is named the same as the Titled Editor key
|
|
97
|
+
# (see TITLE_EDITOR_EA_KEY_PREFIX) with this suffix added.
|
|
98
|
+
# So for the Title Editor key 'xolo-<title>', we'll also have
|
|
99
|
+
# a matching normal EA called 'xolo-<title>-installed-version'
|
|
100
|
+
JAMF_NORMAL_EA_NAME_SUFFIX = '-installed-version'
|
|
101
|
+
|
|
102
|
+
JAMF_INSTALLED_GROUP_NAME_SUFFIX = '-installed'
|
|
103
|
+
JAMF_FROZEN_GROUP_NAME_SUFFIX = '-frozen'
|
|
104
|
+
|
|
105
|
+
JAMF_UNINSTALL_SUFFIX = '-uninstall'
|
|
106
|
+
JAMF_EXPIRE_SUFFIX = '-expire'
|
|
107
|
+
|
|
108
|
+
# the expire policy will run this client command,
|
|
109
|
+
# appending the title
|
|
110
|
+
# We don't specify a full path so that localized installations
|
|
111
|
+
# will work as long as its in roots default path
|
|
112
|
+
# e.g. /usr/local/bin vs /usr/local/pixar/bin
|
|
113
|
+
CLIENT_EXPIRE_COMMAND = 'xolo expire'
|
|
114
|
+
|
|
115
|
+
# When we are given a Self Service icon for the title,
|
|
116
|
+
# we might not be ready to upload it to jamf, cuz until we
|
|
117
|
+
# have a version to pilot, there's nothing IN jamf.
|
|
118
|
+
# So we always store it locally in this file inside the
|
|
119
|
+
# title dir. The extension from the original file will be
|
|
120
|
+
# appended, e.g. '.png'
|
|
121
|
+
SELF_SERVICE_ICON_FILENAME = 'self-service-icon'
|
|
122
|
+
|
|
123
|
+
# The JPAPI endpoint for Patch Titles.
|
|
124
|
+
#
|
|
125
|
+
# ruby-jss still uses the Classic API for Patch Titles, and won't
|
|
126
|
+
# by migrated to JPAPI until Jamf fully implements all aspects of
|
|
127
|
+
# patch management to JPAPI. As of this writing, that's not the case.
|
|
128
|
+
# But, the JPAPI endpoint for Patch Title Reporting returns more
|
|
129
|
+
# detailed data than the Classic API, so we use it here, and will
|
|
130
|
+
# keep using it as we move forward.
|
|
131
|
+
#
|
|
132
|
+
# This is the top-level endpoint for all patch titles,
|
|
133
|
+
# see JPAPI_PATCH_REPORT_RSRC for the reporting endpoint below it.
|
|
134
|
+
#
|
|
135
|
+
# TODO: Remove this and update relevant methods when ruby-jss
|
|
136
|
+
# is updated to use JPAPI for Patch Titles..
|
|
137
|
+
JPAPI_PATCH_TITLE_RSRC = 'v2/patch-software-title-configurations'
|
|
138
|
+
|
|
139
|
+
# The JPAPI endpoint for patch reporting.
|
|
140
|
+
# The JPAPI_PATCH_TITLE_RSRC is appended with "/<id>/#{JPAPI_PATCH_REPORT_RSRC}"
|
|
141
|
+
# to get the URL for the patch report for a specific title.
|
|
142
|
+
#
|
|
143
|
+
# TODO: Remove this and update relevant methods when ruby-jss
|
|
144
|
+
# is updated to use JPAPI for Patch Titles..
|
|
145
|
+
#
|
|
146
|
+
JPAPI_PATCH_REPORT_RSRC = 'patch-report'
|
|
147
|
+
|
|
148
|
+
SELF_SERVICE_INSTALL_BTN_TEXT = 'Install'
|
|
149
|
+
|
|
150
|
+
# Class Methods
|
|
151
|
+
######################
|
|
152
|
+
######################
|
|
153
|
+
|
|
154
|
+
# @return [Array<Pathname>] A list of all known title dirs
|
|
155
|
+
######################
|
|
156
|
+
def self.title_dirs
|
|
157
|
+
TITLES_DIR.children
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Array<String>] A list of all known titles,
|
|
161
|
+
# just the basenames of all the title_dirs
|
|
162
|
+
######################
|
|
163
|
+
def self.all_titles
|
|
164
|
+
title_dirs.map(&:basename).map(&:to_s)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [String] The key and display name of a version script stored
|
|
168
|
+
# in the title editor as the ExtAttr for a given title
|
|
169
|
+
#####################
|
|
170
|
+
def self.ted_ea_key(title)
|
|
171
|
+
"#{TITLE_EDITOR_EA_KEY_PREFIX}#{title}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @return [String] The display name of a version script as a normal
|
|
175
|
+
# EA in Jamf, which can be used in Smart Groups and Adv Searches.
|
|
176
|
+
#####################
|
|
177
|
+
def self.jamf_normal_ea_name(title)
|
|
178
|
+
"#{ted_ea_key(title)}#{JAMF_NORMAL_EA_NAME_SUFFIX}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The title dir for a given title on the server,
|
|
182
|
+
# which may or may not exist.
|
|
183
|
+
#
|
|
184
|
+
# @pararm title [String] the title we care about
|
|
185
|
+
#
|
|
186
|
+
# @return [Pathname]
|
|
187
|
+
#####################
|
|
188
|
+
def self.title_dir(title)
|
|
189
|
+
TITLES_DIR + title
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The the local JSON file containing the current values
|
|
193
|
+
# for the given title
|
|
194
|
+
#
|
|
195
|
+
# @pararm title [String] the title we care about
|
|
196
|
+
#
|
|
197
|
+
# @return [Pathname]
|
|
198
|
+
#####################
|
|
199
|
+
def self.title_data_file(title)
|
|
200
|
+
title_dir(title) + "#{title}.json"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @pararm title [String] the title we care about
|
|
204
|
+
#
|
|
205
|
+
# @return [Pathname] The the local file containing the code of the version script
|
|
206
|
+
#####################
|
|
207
|
+
def self.version_script_file(title)
|
|
208
|
+
title_dir(title) + VERSION_SCRIPT_FILENAME
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @pararm title [String] the title we care about
|
|
212
|
+
#
|
|
213
|
+
# @return [Pathname] The the local file containing the code of the version script
|
|
214
|
+
#####################
|
|
215
|
+
def self.uninstall_script_file(title)
|
|
216
|
+
title_dir(title) + UNINSTALL_SCRIPT_FILENAME
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @pararm title [String] the title we care about
|
|
220
|
+
#
|
|
221
|
+
# @return [Pathname] The the local file containing the self-service icon
|
|
222
|
+
#####################
|
|
223
|
+
def self.ssvc_icon_file(title)
|
|
224
|
+
title_dir(title).children.select { |c| c.basename.to_s.start_with? SELF_SERVICE_ICON_FILENAME }.first
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Instantiate from the local JSON file containing the current values
|
|
228
|
+
# for the given title
|
|
229
|
+
#
|
|
230
|
+
# NOTE: All instantiation should happen using the #instantiate_title method
|
|
231
|
+
# in the server app instance. Please don't call this method directly
|
|
232
|
+
#
|
|
233
|
+
# @pararm title [String] the title we care about
|
|
234
|
+
# @return [Xolo::Server::Title] load an existing title
|
|
235
|
+
# from the on-disk JSON file
|
|
236
|
+
######################
|
|
237
|
+
def self.load(title)
|
|
238
|
+
Xolo::Server.logger.debug "Loading title '#{title}' from file"
|
|
239
|
+
new parse_json(title_data_file(title).read)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# @param title [String] the title we are looking for
|
|
243
|
+
# @pararm cnx [Windoo::Connection] The Title Editor connection to use
|
|
244
|
+
# @return [Boolean] Does the given title exist in the Title Editor?
|
|
245
|
+
###############################
|
|
246
|
+
def self.in_ted?(title, cnx:)
|
|
247
|
+
Windoo::SoftwareTitle.all_ids(cnx: cnx).include? title
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Is a title locked for updates?
|
|
251
|
+
#############################
|
|
252
|
+
def self.locked?(title)
|
|
253
|
+
curr_lock = Xolo::Server.object_locks.dig title, :expires
|
|
254
|
+
curr_lock && curr_lock > Time.now
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Attributes
|
|
258
|
+
######################
|
|
259
|
+
######################
|
|
260
|
+
|
|
261
|
+
# For each title there will be a smart group containing all macs
|
|
262
|
+
# that have any version of the title installed. The smart group
|
|
263
|
+
# will be named 'xolo-<title>-installed'
|
|
264
|
+
#
|
|
265
|
+
# It will be used as an exclusion for the initial auto-installation
|
|
266
|
+
# policy for each version since if the title is installed at all,
|
|
267
|
+
# any installation is not 'initial' but an update, and will be
|
|
268
|
+
# handled by the Patch Policy.
|
|
269
|
+
#
|
|
270
|
+
# Since there is one such group per title, it's name is stored here
|
|
271
|
+
#
|
|
272
|
+
# @return [String] the name of the smart group
|
|
273
|
+
attr_reader :jamf_installed_group_name
|
|
274
|
+
|
|
275
|
+
# For each title there will be a static group containing macs
|
|
276
|
+
# that should not get any automatic installs or updates, They
|
|
277
|
+
# should be 'frozen' at whatever version was installed when they
|
|
278
|
+
# were added to the group. It will be named 'xolo-<title>-frozen'
|
|
279
|
+
#
|
|
280
|
+
# It will be used as an exclusion for the installation
|
|
281
|
+
# policies and the patch policy for each version.
|
|
282
|
+
#
|
|
283
|
+
# Membership is maintained using `xadm freeze <title> <computer> [<computer> ...]`
|
|
284
|
+
# and `xadm thaw <title> <computer> [<computer> ...]`
|
|
285
|
+
#
|
|
286
|
+
# Use `xadm report <title> frozen` to see a list.
|
|
287
|
+
#
|
|
288
|
+
# If computer groups are used with freeze/thaw, they are expanded and their members
|
|
289
|
+
# added/removed individually in the static group
|
|
290
|
+
#
|
|
291
|
+
# Since there is one such group per title, it's name is stored here
|
|
292
|
+
#
|
|
293
|
+
# @return [String] the name of the smart group
|
|
294
|
+
attr_reader :jamf_frozen_group_name
|
|
295
|
+
|
|
296
|
+
# The name of the policy that does initial manual or self-service
|
|
297
|
+
# installs of the currently-released version of this title.
|
|
298
|
+
# It will be named 'xolo-<title>-install'
|
|
299
|
+
attr_reader :jamf_manual_install_released_policy_name
|
|
300
|
+
|
|
301
|
+
# If a title is uninstallable, it will have a script in Jamf
|
|
302
|
+
# named 'xolo-<title>-uninstall'
|
|
303
|
+
#
|
|
304
|
+
# @return [String] the name of the script to uninstall the title
|
|
305
|
+
attr_reader :jamf_uninstall_script_name
|
|
306
|
+
|
|
307
|
+
# If a title is uninstallable, it will have a policy in Jamf
|
|
308
|
+
# named 'xolo-<title>-uninstall' that will run the script of
|
|
309
|
+
# the same name, using a trigger of the same name.
|
|
310
|
+
#
|
|
311
|
+
# @return [String] the name of the policy to uninstall the title
|
|
312
|
+
attr_reader :jamf_uninstall_policy_name
|
|
313
|
+
|
|
314
|
+
# If a title is expirable, it will have a policy in Jamf
|
|
315
|
+
# named 'xolo-<title>-expire' that will run the expiration
|
|
316
|
+
# process daily, at checkin or using a trigger of the same name.
|
|
317
|
+
#
|
|
318
|
+
# @return [String] the name of the policy to uninstall the title
|
|
319
|
+
attr_reader :jamf_expire_policy_name
|
|
320
|
+
|
|
321
|
+
# The instance of Xolo::Server::App that instantiated this
|
|
322
|
+
# title object. This is how we access things that are available in routes
|
|
323
|
+
# and helpers, like the single Jamf and TEd
|
|
324
|
+
# connections for this App instance.
|
|
325
|
+
# @return [Xolo::Server::App] our Sinatra server app
|
|
326
|
+
attr_accessor :server_app_instance
|
|
327
|
+
|
|
328
|
+
# @return [Integer] The Windoo::SoftwareTitle#softwareTitleId
|
|
329
|
+
attr_accessor :ted_id_number
|
|
330
|
+
|
|
331
|
+
# when applying updates, the new data from xadm is stored
|
|
332
|
+
# here so it can be accessed by update-methods
|
|
333
|
+
# and compared to the current instance values
|
|
334
|
+
# both for updating the title, and the versions
|
|
335
|
+
#
|
|
336
|
+
# @return [Hash] The new data to apply as an update
|
|
337
|
+
attr_reader :new_data_for_update
|
|
338
|
+
|
|
339
|
+
# Also when applying updates, this will hold the
|
|
340
|
+
# changes being made: the differences between
|
|
341
|
+
# tne current attributes and the new_data_for_update
|
|
342
|
+
# We'll figure this out at the start of the update
|
|
343
|
+
# and can use it later to
|
|
344
|
+
# 1) avoid doing things we don't need to
|
|
345
|
+
# 2) log the changes in the change log at the very end
|
|
346
|
+
#
|
|
347
|
+
# This is a Hash with keys of the attribute names that have changed
|
|
348
|
+
# the values are Hashes with keys of :old and :new
|
|
349
|
+
# e.g. { release_groups: { old: ['foo'], new: ['bar'] } }
|
|
350
|
+
#
|
|
351
|
+
# @return [Hash] The changes being made
|
|
352
|
+
attr_reader :changes_for_update
|
|
353
|
+
|
|
354
|
+
# @return [Integer] The Jamf Pro ID for the self-service icon
|
|
355
|
+
# once it has been uploaded
|
|
356
|
+
attr_accessor :ssvc_icon_id
|
|
357
|
+
|
|
358
|
+
# @return [Symbol] The current action being taken on this title
|
|
359
|
+
# one of :creating, :updating, :deleting
|
|
360
|
+
attr_accessor :current_action
|
|
361
|
+
|
|
362
|
+
# @return [String] If current action is :releasing, this is the
|
|
363
|
+
# version being released
|
|
364
|
+
attr_accessor :releasing_version
|
|
365
|
+
|
|
366
|
+
# version_order is defined in ATTRIBUTES
|
|
367
|
+
alias versions version_order
|
|
368
|
+
|
|
369
|
+
# Constructor
|
|
370
|
+
######################
|
|
371
|
+
######################
|
|
372
|
+
|
|
373
|
+
# NOTE: be sure to only instantiate these using the
|
|
374
|
+
# servers 'instantiate_title' method, or else
|
|
375
|
+
# they might not have all the correct innards
|
|
376
|
+
def initialize(data_hash)
|
|
377
|
+
super
|
|
378
|
+
|
|
379
|
+
@ted_id_number ||= data_hash[:ted_id_number]
|
|
380
|
+
@jamf_patch_title_id ||= data_hash[:jamf_patch_title_id]
|
|
381
|
+
@version_order ||= []
|
|
382
|
+
@new_data_for_update = {}
|
|
383
|
+
@changes_for_update = {}
|
|
384
|
+
@jamf_installed_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_INSTALLED_GROUP_NAME_SUFFIX}"
|
|
385
|
+
@jamf_frozen_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_FROZEN_GROUP_NAME_SUFFIX}"
|
|
386
|
+
|
|
387
|
+
@jamf_manual_install_released_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}-install"
|
|
388
|
+
|
|
389
|
+
@jamf_uninstall_script_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
|
|
390
|
+
@jamf_uninstall_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
|
|
391
|
+
@jamf_expire_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_EXPIRE_SUFFIX}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Instance Methods
|
|
395
|
+
######################
|
|
396
|
+
######################
|
|
397
|
+
|
|
398
|
+
# @return [Hash]
|
|
399
|
+
###################
|
|
400
|
+
def session
|
|
401
|
+
server_app_instance&.session || {}
|
|
402
|
+
# @session ||= {}
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# @return [String]
|
|
406
|
+
###################
|
|
407
|
+
def admin
|
|
408
|
+
session[:admin]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# @return [Boolean] Are we creating this title?
|
|
412
|
+
###################
|
|
413
|
+
def creating?
|
|
414
|
+
current_action == :creating
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# @return [Boolean] Are we updating this title?
|
|
418
|
+
###################
|
|
419
|
+
def updating?
|
|
420
|
+
current_action == :updating
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# @return [Boolean] Are we repairing this title?
|
|
424
|
+
###################
|
|
425
|
+
def repairing?
|
|
426
|
+
current_action == :repairing
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# @return [Boolean] Are we deleting this title?
|
|
430
|
+
###################
|
|
431
|
+
def deleting?
|
|
432
|
+
current_action == :deleting
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# @return [Boolean] Are we releasing a version this title?
|
|
436
|
+
###################
|
|
437
|
+
def releasing?
|
|
438
|
+
current_action == :releasing
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Append a message to the progress stream file,
|
|
442
|
+
# optionally sending it also to the log
|
|
443
|
+
#
|
|
444
|
+
# @param message [String] the message to append
|
|
445
|
+
# @param log [Symbol] the level at which to log the message
|
|
446
|
+
# one of :debug, :info, :warn, :error, :fatal, or :unknown.
|
|
447
|
+
# Default is nil, which doesn't log the message at all.
|
|
448
|
+
#
|
|
449
|
+
# @return [void]
|
|
450
|
+
###################
|
|
451
|
+
def progress(msg, log: :debug)
|
|
452
|
+
server_app_instance.progress msg, log: log
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# @return [Windoo::Connection] a single Title Editor connection to use for
|
|
456
|
+
# the life of this instance
|
|
457
|
+
#############################
|
|
458
|
+
def ted_cnx
|
|
459
|
+
server_app_instance.ted_cnx
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# @return [Jamf::Connection] a single Jamf Pro API connection to use for
|
|
463
|
+
# the life of this instance
|
|
464
|
+
#############################
|
|
465
|
+
def jamf_cnx(refresh: false)
|
|
466
|
+
server_app_instance.jamf_cnx refresh: refresh
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# The title dir for this title on the server
|
|
470
|
+
# @return [Pathname]
|
|
471
|
+
#########################
|
|
472
|
+
def title_dir
|
|
473
|
+
@title_dir ||= self.class.title_dir title
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# The title data file for this title on the server
|
|
477
|
+
# @return [Pathname]
|
|
478
|
+
#########################
|
|
479
|
+
def title_data_file
|
|
480
|
+
@title_data_file ||= self.class.title_data_file title
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# @return [Pathname] The the local file containing the self-service icon
|
|
484
|
+
#####################
|
|
485
|
+
def ssvc_icon_file
|
|
486
|
+
@ssvc_icon_file ||= self.class.ssvc_icon_file title
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# @return [Pathname] The the local file containing the code of the version script
|
|
490
|
+
#####################
|
|
491
|
+
def version_script_file
|
|
492
|
+
@version_script_file ||= self.class.version_script_file title
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# The code of the version script, if any,
|
|
496
|
+
# considering the new data of any changes being made
|
|
497
|
+
#
|
|
498
|
+
# Returns nil if there is no version script, or if we are in the
|
|
499
|
+
# process of deleting it.
|
|
500
|
+
#
|
|
501
|
+
# @return [String] The string contents of the version_script, if any
|
|
502
|
+
####################
|
|
503
|
+
def version_script_contents
|
|
504
|
+
return @version_script_contents if defined? @version_script_contents
|
|
505
|
+
|
|
506
|
+
curr_script =
|
|
507
|
+
if changes_for_update&.key? :version_script
|
|
508
|
+
# new, incoming script
|
|
509
|
+
changes_for_update[:version_script][:new]
|
|
510
|
+
else
|
|
511
|
+
# the current attribute value, might be Xolo::ITEM_UPLOADED
|
|
512
|
+
version_script
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
@version_script_contents =
|
|
516
|
+
if curr_script.pix_empty?
|
|
517
|
+
# no script, or deleting script
|
|
518
|
+
nil
|
|
519
|
+
elsif curr_script == Xolo::ITEM_UPLOADED
|
|
520
|
+
# use the one we have saved on disk
|
|
521
|
+
version_script_file.read
|
|
522
|
+
else
|
|
523
|
+
# this will be a new one from the changes_for_update
|
|
524
|
+
curr_script
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# @return [Pathname] The the local file containing the code of the uninstall script
|
|
529
|
+
#####################
|
|
530
|
+
def uninstall_script_file
|
|
531
|
+
@uninstall_script_file ||= self.class.uninstall_script_file title
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# The code of the uninstall_script , if any,
|
|
535
|
+
# considering the new data of any changes being made
|
|
536
|
+
#
|
|
537
|
+
# Returns nil if there is no uninstall_script, or if we are in the
|
|
538
|
+
# process of deleting it.
|
|
539
|
+
#
|
|
540
|
+
# @return [String] The string contents of the uninstall_script, if any
|
|
541
|
+
####################
|
|
542
|
+
def uninstall_script_contents
|
|
543
|
+
return @uninstall_script_contents if defined? @uninstall_script_contents
|
|
544
|
+
|
|
545
|
+
# use any new/incoming value if we have any
|
|
546
|
+
# this might still be nil or an empty array if we are removing uninstallability
|
|
547
|
+
curr_script = changes_for_update.dig(:uninstall_script, :new) || changes_for_update.dig(:uninstall_ids, :new)
|
|
548
|
+
curr_script = nil if curr_script.pix_empty?
|
|
549
|
+
|
|
550
|
+
# otherwise use the existing value
|
|
551
|
+
curr_script ||= uninstall_script || uninstall_ids
|
|
552
|
+
|
|
553
|
+
# now get the actual script
|
|
554
|
+
@uninstall_script_contents =
|
|
555
|
+
if curr_script.pix_empty?
|
|
556
|
+
# removing uninstallability, or it was never added
|
|
557
|
+
nil
|
|
558
|
+
elsif curr_script == Xolo::ITEM_UPLOADED
|
|
559
|
+
# nothing changed, use the one we have saved on disk
|
|
560
|
+
uninstall_script_file.read
|
|
561
|
+
else
|
|
562
|
+
# this will be a new one from the changes_for_update
|
|
563
|
+
generate_uninstall_script curr_script
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# log_debug "Uninstall script contents: #{@uninstall_script_contents}"
|
|
567
|
+
@uninstall_script_contents
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# @param script_or_pkg_ids [String] The new uninstall script, or comma-separated list of pkg IDs
|
|
571
|
+
# @return [String, Array ] The uninstall script, provided or generated from the given pkg ids
|
|
572
|
+
#####################
|
|
573
|
+
def generate_uninstall_script(script_or_pkg_ids)
|
|
574
|
+
# Its already a script, validated by xadm to start with #!
|
|
575
|
+
return script_or_pkg_ids if script_or_pkg_ids.is_a? String
|
|
576
|
+
|
|
577
|
+
uninstall_script_template.sub 'PKG_IDS_FROM_XOLO_GO_HERE', script_or_pkg_ids.join(' ')
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# @return [String] The template zsh script for uninstalling via pkgutil
|
|
581
|
+
#####################
|
|
582
|
+
def uninstall_script_template
|
|
583
|
+
# parent 1 = server
|
|
584
|
+
# parent 2 = xolo
|
|
585
|
+
# parent 3 = lib
|
|
586
|
+
# parent 4 = xolo gem
|
|
587
|
+
data_dir = Pathname.new(__FILE__).parent.parent.parent.parent + 'data'
|
|
588
|
+
template_file = data_dir + 'uninstall-pkgs-by-id.zsh'
|
|
589
|
+
template_file.read
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# @return [String] The display name of a version script as a normal
|
|
593
|
+
# EA in Jamf, which can be used in Smart Groups and Adv Searches.
|
|
594
|
+
#####################
|
|
595
|
+
def jamf_normal_ea_name
|
|
596
|
+
@jamf_normal_ea_name ||= self.class.jamf_normal_ea_name title
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# prepend a new version to the version_order
|
|
600
|
+
#
|
|
601
|
+
# @param version [String] the version to prepend
|
|
602
|
+
#
|
|
603
|
+
# @return [void]
|
|
604
|
+
########################
|
|
605
|
+
def prepend_version(version)
|
|
606
|
+
lock
|
|
607
|
+
version_order.unshift version
|
|
608
|
+
save_local_data
|
|
609
|
+
ensure
|
|
610
|
+
unlock
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# remove a version from the version_order
|
|
614
|
+
#
|
|
615
|
+
# @param version [String] the version to remove
|
|
616
|
+
#
|
|
617
|
+
# @return [void]
|
|
618
|
+
########################
|
|
619
|
+
def remove_version(version)
|
|
620
|
+
lock
|
|
621
|
+
version_order.delete version
|
|
622
|
+
save_local_data
|
|
623
|
+
ensure
|
|
624
|
+
unlock
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# instantiate a version if this title
|
|
628
|
+
#
|
|
629
|
+
# @return [Xolo::Server::Version]
|
|
630
|
+
########################
|
|
631
|
+
def version_object(version)
|
|
632
|
+
log_debug "Instantiating version #{version} from Title instance #{title}"
|
|
633
|
+
server_app_instance.instantiate_version(title: self, version: version)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# @return [Array<Xolo::Server::Version>] An array of all current version objects
|
|
637
|
+
# NOTE: This might not be wise if hundreds of versions, but automated cleanup should
|
|
638
|
+
# help with that.
|
|
639
|
+
########################
|
|
640
|
+
def version_objects(refresh: false)
|
|
641
|
+
@version_objects = nil if refresh
|
|
642
|
+
return @version_objects if @version_objects
|
|
643
|
+
|
|
644
|
+
@version_objects = version_order.map do |v|
|
|
645
|
+
version_object v
|
|
646
|
+
rescue Xolo::Core::Exceptions::NoSuchItemError
|
|
647
|
+
next if deleting?
|
|
648
|
+
|
|
649
|
+
raise
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# @return [String] The URL path for the patch report for this title
|
|
654
|
+
#############################
|
|
655
|
+
def patch_report_rsrc
|
|
656
|
+
@patch_report_rsrc ||= "#{JPAPI_PATCH_TITLE_RSRC}/#{jamf_patch_title_id}/#{JPAPI_PATCH_REPORT_RSRC}"
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Save a new title, adding to the
|
|
660
|
+
# local filesystem, Jamf Pro, and the Title Editor as needed
|
|
661
|
+
#
|
|
662
|
+
# @return [void]
|
|
663
|
+
#########################
|
|
664
|
+
def create
|
|
665
|
+
lock
|
|
666
|
+
|
|
667
|
+
@current_action = :creating
|
|
668
|
+
|
|
669
|
+
self.creation_date = Time.now
|
|
670
|
+
self.created_by = admin
|
|
671
|
+
log_debug "creation_date: #{creation_date}, created_by: #{created_by}"
|
|
672
|
+
|
|
673
|
+
# this will create the title as needed in the Title Editor
|
|
674
|
+
create_title_in_ted
|
|
675
|
+
create_title_in_jamf
|
|
676
|
+
|
|
677
|
+
# save to file last, because saving to TitleEd and Jamf will
|
|
678
|
+
# add some data
|
|
679
|
+
progress 'Saving title data to Xolo server'
|
|
680
|
+
save_local_data
|
|
681
|
+
|
|
682
|
+
log_change msg: 'Title Created'
|
|
683
|
+
|
|
684
|
+
# ssvc icon is uploaded in a separate process, and the
|
|
685
|
+
# title data file will be updated as needed then.
|
|
686
|
+
ensure
|
|
687
|
+
unlock
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Update this title, updating to the
|
|
691
|
+
# local filesystem, Jamf Pro, and the Title Editor,
|
|
692
|
+
# and applying any changes to existing versions as needed.
|
|
693
|
+
#
|
|
694
|
+
# @param new_data [Hash] The new data sent from xadm
|
|
695
|
+
# @return [void]
|
|
696
|
+
#########################
|
|
697
|
+
def update(new_data)
|
|
698
|
+
lock
|
|
699
|
+
|
|
700
|
+
@current_action = :updating
|
|
701
|
+
@new_data_for_update = new_data
|
|
702
|
+
@changes_for_update = note_changes_for_update_and_log
|
|
703
|
+
|
|
704
|
+
if @changes_for_update.pix_empty?
|
|
705
|
+
progress 'No changes to make', log: :info
|
|
706
|
+
return
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
log_info "Updating title '#{title}' for admin '#{admin}'"
|
|
710
|
+
log_debug "Updating title with these changes: #{changes_for_update}"
|
|
711
|
+
|
|
712
|
+
# changelog - log the changes now, and
|
|
713
|
+
# if there is an error, we'll log that too
|
|
714
|
+
# saying the above changes were not completed and to
|
|
715
|
+
# look at the server log for details.
|
|
716
|
+
log_update_changes
|
|
717
|
+
|
|
718
|
+
# Do ted before doing things in Jamf
|
|
719
|
+
update_title_in_ted
|
|
720
|
+
update_title_in_jamf
|
|
721
|
+
update_local_instance_values
|
|
722
|
+
save_local_data
|
|
723
|
+
|
|
724
|
+
# if we already have a version script, and it hasn't changed, the new data should
|
|
725
|
+
# contain Xolo::ITEM_UPLOADED. If its nil, we shouldn't
|
|
726
|
+
# have one at all and should remove the old one if its there
|
|
727
|
+
delete_version_script_file unless new_data_for_update[:version_script]
|
|
728
|
+
|
|
729
|
+
# Do This at the end - after all the versions/patches have been updated.
|
|
730
|
+
# Jamf won't see the need for re-acceptance until after the title
|
|
731
|
+
# (and at least one patch) have been re-enabled.
|
|
732
|
+
accept_jamf_patch_ea if need_to_accept_jamf_patch_ea?
|
|
733
|
+
|
|
734
|
+
# any new self svc icon will be uploaded in a separate process
|
|
735
|
+
# and the local data will be updated again then
|
|
736
|
+
#
|
|
737
|
+
rescue => e
|
|
738
|
+
log_change msg: "ERROR: The update failed and the changes didn't all go through!\n#{e.class}: #{e.message}\nSee server log for details."
|
|
739
|
+
|
|
740
|
+
# re-raise for proper error handling in the server app
|
|
741
|
+
raise
|
|
742
|
+
ensure
|
|
743
|
+
unlock
|
|
744
|
+
end # update
|
|
745
|
+
|
|
746
|
+
# Update our instance attributes with any new data before
|
|
747
|
+
# saving the changes back out to the file system
|
|
748
|
+
# @return [void]
|
|
749
|
+
###########################
|
|
750
|
+
def update_local_instance_values
|
|
751
|
+
# update instance data with new data before writing out to the filesystem.
|
|
752
|
+
# Do this last so that the instance values can be compared to
|
|
753
|
+
# new_data_for_update in the steps above.
|
|
754
|
+
# Also, those steps might have updated some server-specific attributes
|
|
755
|
+
# which will be saved to the file system as well.
|
|
756
|
+
ATTRIBUTES.each do |attr, deets|
|
|
757
|
+
# make sure these are updated elsewhere if needed,
|
|
758
|
+
# e.g. modification data.
|
|
759
|
+
next if deets[:read_only]
|
|
760
|
+
next unless deets[:cli]
|
|
761
|
+
|
|
762
|
+
new_val = new_data_for_update[attr]
|
|
763
|
+
old_val = send(attr)
|
|
764
|
+
next if new_val == old_val
|
|
765
|
+
|
|
766
|
+
log_debug "Updating Xolo Title attribute '#{attr}': '#{old_val}' -> '#{new_val}'"
|
|
767
|
+
send "#{attr}=", new_val
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# update any other server-specific attributes here...
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Save our current data out to our JSON data file
|
|
774
|
+
# This overwrites the existing data.
|
|
775
|
+
#
|
|
776
|
+
# @return [void]
|
|
777
|
+
##########################
|
|
778
|
+
def save_local_data
|
|
779
|
+
# create the dirs for the title
|
|
780
|
+
title_dir.mkpath
|
|
781
|
+
vdir = title_dir + Xolo::Server::Version::VERSIONS_DIRNAME
|
|
782
|
+
vdir.mkpath
|
|
783
|
+
|
|
784
|
+
save_version_script
|
|
785
|
+
save_uninstall_script
|
|
786
|
+
|
|
787
|
+
self.modification_date = Time.now
|
|
788
|
+
self.modified_by = admin
|
|
789
|
+
log_debug "Title '#{title}' noting modification by #{modified_by}"
|
|
790
|
+
|
|
791
|
+
# do we have a stored self service icon?
|
|
792
|
+
self.self_service_icon = ssvc_icon_file ? Xolo::ITEM_UPLOADED : nil
|
|
793
|
+
|
|
794
|
+
log_debug "Saving local title data to: #{title_data_file}"
|
|
795
|
+
title_data_file.pix_atomic_write to_json
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# Save our current version script out to our local file,
|
|
799
|
+
# but only if we aren't using app_name and app_bundle_id
|
|
800
|
+
# and only if it's changed
|
|
801
|
+
#
|
|
802
|
+
# This won't delete the script if it's being removed, that
|
|
803
|
+
# happens elsewhere.
|
|
804
|
+
#
|
|
805
|
+
# This overwrites the existing data.
|
|
806
|
+
#
|
|
807
|
+
# @return [void]
|
|
808
|
+
##########################
|
|
809
|
+
def save_version_script
|
|
810
|
+
return if app_name || app_bundle_id
|
|
811
|
+
return if version_script_contents.nil?
|
|
812
|
+
|
|
813
|
+
log_debug "Saving version_script to: #{version_script_file}"
|
|
814
|
+
version_script_file.pix_atomic_write version_script_contents
|
|
815
|
+
|
|
816
|
+
# the json file only stores 'uploaded' in the version_script attr.
|
|
817
|
+
self.version_script = Xolo::ITEM_UPLOADED
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# Save our current uninstall script out to our local file.
|
|
821
|
+
#
|
|
822
|
+
# This won't delete the script if it's being removed, that
|
|
823
|
+
# happens elsewhere.
|
|
824
|
+
#
|
|
825
|
+
# This overwrites the existing data.
|
|
826
|
+
#
|
|
827
|
+
# @return [void]
|
|
828
|
+
##########################
|
|
829
|
+
def save_uninstall_script
|
|
830
|
+
return if uninstall_script == Xolo::ITEM_UPLOADED || uninstall_ids == Xolo::ITEM_UPLOADED
|
|
831
|
+
return if uninstall_script_contents.nil?
|
|
832
|
+
|
|
833
|
+
log_debug "Saving uninstall script to: #{uninstall_script_file}"
|
|
834
|
+
uninstall_script_file.pix_atomic_write uninstall_script_contents
|
|
835
|
+
|
|
836
|
+
# the json file only stores 'uploaded' in uninstall_script
|
|
837
|
+
# The actual script is saved in its own file.
|
|
838
|
+
self.uninstall_script &&= Xolo::ITEM_UPLOADED
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# are we uninstallable?
|
|
842
|
+
#
|
|
843
|
+
# @return [Boolean]
|
|
844
|
+
##########################
|
|
845
|
+
def uninstallable?
|
|
846
|
+
uninstall_script || !uninstall_ids.pix_empty?
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
# Save the self_service_icon from the upload tmpfile
|
|
850
|
+
# to the file in the data dir.
|
|
851
|
+
#
|
|
852
|
+
# This is run by the upload route, not the
|
|
853
|
+
# create or update methods here.
|
|
854
|
+
# xadm does the upload after creating or updating the title
|
|
855
|
+
#
|
|
856
|
+
# @param tempfile [Pathname] The path to the uploaded tmp file
|
|
857
|
+
#
|
|
858
|
+
# @return [void]
|
|
859
|
+
##########################
|
|
860
|
+
def save_ssvc_icon(tempfile, orig_filename)
|
|
861
|
+
lock
|
|
862
|
+
# here's where we'll store it on the server
|
|
863
|
+
ext_for_file = orig_filename.split(Xolo::DOT).last
|
|
864
|
+
new_basename = "#{SELF_SERVICE_ICON_FILENAME}.#{ext_for_file}"
|
|
865
|
+
new_icon_file = title_dir + new_basename
|
|
866
|
+
|
|
867
|
+
# delete any previous icon files
|
|
868
|
+
existing_icon_file = ssvc_icon_file
|
|
869
|
+
if existing_icon_file&.file?
|
|
870
|
+
log_debug "Deleting older icon file: #{existing_icon_file.basename}"
|
|
871
|
+
existing_icon_file.delete
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
log_debug "Saving self_service_icon '#{orig_filename}' to: #{new_basename}"
|
|
875
|
+
tempfile.rename new_icon_file
|
|
876
|
+
|
|
877
|
+
# the json file only stores 'uploaded' in the self_service_icon
|
|
878
|
+
# attr.
|
|
879
|
+
self.self_service_icon = Xolo::ITEM_UPLOADED
|
|
880
|
+
save_local_data
|
|
881
|
+
ensure
|
|
882
|
+
unlock
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Delete the version script file
|
|
886
|
+
#
|
|
887
|
+
# @return [void]
|
|
888
|
+
##########################
|
|
889
|
+
def delete_version_script_file
|
|
890
|
+
return unless version_script_file.file?
|
|
891
|
+
|
|
892
|
+
log_debug "Deleting version script file: #{version_script_file}"
|
|
893
|
+
|
|
894
|
+
version_script_file.delete
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
# Delete the title and all of its version
|
|
898
|
+
# @return [void]
|
|
899
|
+
##########################
|
|
900
|
+
def delete
|
|
901
|
+
lock
|
|
902
|
+
@current_action = :deleting
|
|
903
|
+
|
|
904
|
+
progress "Deleting all versions of #{title}...", log: :debug
|
|
905
|
+
# Delete them in reverse order (oldest first) so the jamf server doesn't
|
|
906
|
+
# see each older version as being 'released' again as newer
|
|
907
|
+
# ones are deleted.
|
|
908
|
+
version_objects.reverse.each do |vers|
|
|
909
|
+
# vers might be nil if it was already deleted
|
|
910
|
+
# e.g. a prev. attempt to delete the title failed partway through
|
|
911
|
+
vers&.delete update_title: false
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
delete_title_from_ted
|
|
915
|
+
|
|
916
|
+
delete_title_from_jamf
|
|
917
|
+
|
|
918
|
+
delete_changelog
|
|
919
|
+
|
|
920
|
+
progress "Deleting Xolo server data for title '#{title}'", log: :info
|
|
921
|
+
title_dir.rmtree
|
|
922
|
+
ensure
|
|
923
|
+
unlock
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# Release a version of this title
|
|
927
|
+
#
|
|
928
|
+
# @param version_to_release [String] the version to release
|
|
929
|
+
#
|
|
930
|
+
# @return [void]
|
|
931
|
+
##########################
|
|
932
|
+
def release(version_to_release)
|
|
933
|
+
lock
|
|
934
|
+
@current_action = :releasing
|
|
935
|
+
@releasing_version = version_to_release
|
|
936
|
+
|
|
937
|
+
validate_release(version_to_release)
|
|
938
|
+
|
|
939
|
+
progress "Releasing version #{version_to_release} of title '#{title}'", log: :info
|
|
940
|
+
|
|
941
|
+
update_versions_for_release version_to_release
|
|
942
|
+
|
|
943
|
+
# update the title
|
|
944
|
+
self.released_version = version_to_release
|
|
945
|
+
save_local_data
|
|
946
|
+
ensure
|
|
947
|
+
unlock
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
# are we OK releasing a given version?
|
|
951
|
+
# @return [void]
|
|
952
|
+
######################################
|
|
953
|
+
def validate_release(version_to_release)
|
|
954
|
+
if released_version == version_to_release
|
|
955
|
+
raise Xolo::InvalidDataError,
|
|
956
|
+
"Version '#{version_to_release}' of title '#{title}' is already released"
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
return if versions.include? version_to_release
|
|
960
|
+
|
|
961
|
+
raise Xolo::NoSuchItemError,
|
|
962
|
+
"No version '#{version_to_release}' for title '#{title}'"
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Update all versions when releasing one
|
|
966
|
+
# @param version_to_release [String] the version to release
|
|
967
|
+
# @return [void]
|
|
968
|
+
##############################
|
|
969
|
+
def update_versions_for_release(version_to_release)
|
|
970
|
+
# get the Version objects and figure out our starting point, but process
|
|
971
|
+
# them in reverse order so that we don't have two released versions at once
|
|
972
|
+
all_versions = version_objects.reverse
|
|
973
|
+
vobj_to_release = all_versions.find { |v| v.version == version_to_release }
|
|
974
|
+
vobj_current_release = all_versions.find { |v| v.version == released_version }
|
|
975
|
+
|
|
976
|
+
rollback = vobj_current_release && vobj_to_release < vobj_current_release
|
|
977
|
+
|
|
978
|
+
progress "Rolling back from version #{released_version}", log: :info if rollback
|
|
979
|
+
|
|
980
|
+
all_versions.each do |vobj|
|
|
981
|
+
# This is the one we are releasing
|
|
982
|
+
if vobj == vobj_to_release
|
|
983
|
+
release_version(vobj, rollback: rollback)
|
|
984
|
+
|
|
985
|
+
# This one is older than the one we're releasing
|
|
986
|
+
# so its either deprecated or skipped
|
|
987
|
+
elsif vobj < vobj_to_release
|
|
988
|
+
deprecate_or_skip_version(vobj)
|
|
989
|
+
|
|
990
|
+
# this one is newer than the one we're releasing
|
|
991
|
+
# revert to pilot if appropriate
|
|
992
|
+
else
|
|
993
|
+
reset_version_to_pilot(vobj)
|
|
994
|
+
|
|
995
|
+
end # if vobj == vobj_to_release
|
|
996
|
+
end # all_versions.each
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
# release a specific version
|
|
1000
|
+
# @param vobj [Xolo::Server::Version] the version object to be released
|
|
1001
|
+
# @return [void]
|
|
1002
|
+
#######################################
|
|
1003
|
+
def release_version(vobj, rollback:)
|
|
1004
|
+
vobj.release rollback: rollback
|
|
1005
|
+
|
|
1006
|
+
# update the jamf_manual_install_released_policy to install this version
|
|
1007
|
+
msg = "Jamf: Setting policy #{jamf_manual_install_released_policy_name} to install the package for version '#{vobj.version}'"
|
|
1008
|
+
progress msg, log: :info
|
|
1009
|
+
|
|
1010
|
+
pol = jamf_manual_install_released_policy
|
|
1011
|
+
pol.package_ids.each { |pid| pol.remove_package pid }
|
|
1012
|
+
pol.add_package vobj.jamf_pkg_id
|
|
1013
|
+
pol.save
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# Deprecate or skip a version
|
|
1017
|
+
# @param vobj [Xolo::Server::Version] the version object to be deprecated or skipped
|
|
1018
|
+
# @return [void]
|
|
1019
|
+
#######################################
|
|
1020
|
+
def deprecate_or_skip_version(vobj)
|
|
1021
|
+
# don't do anything if the status is already deprecated or skipped
|
|
1022
|
+
|
|
1023
|
+
# but if its released, we need to deprecate it
|
|
1024
|
+
vobj.deprecate if vobj.status == Xolo::Server::Version::STATUS_RELEASED
|
|
1025
|
+
|
|
1026
|
+
# and skip it if its in pilot
|
|
1027
|
+
vobj.skip if vobj.status == Xolo::Server::Version::STATUS_PILOT
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
# reset a version to pilot status, this happens when rolling back
|
|
1031
|
+
# (releasing a version older than the current release)
|
|
1032
|
+
# @param vobj [Xolo::Server::Version] the version object to be deprecated or skipped
|
|
1033
|
+
# @return [void]
|
|
1034
|
+
#############################
|
|
1035
|
+
def reset_version_to_pilot(vobj)
|
|
1036
|
+
# do nothing if its in pilot
|
|
1037
|
+
return if vobj.status == Xolo::Server::Version::STATUS_PILOT
|
|
1038
|
+
|
|
1039
|
+
# this should be redundant with the above?
|
|
1040
|
+
return unless rollback
|
|
1041
|
+
|
|
1042
|
+
# if we're here, we're rolling back to something older than this
|
|
1043
|
+
# version, and this version is currently released, deprecated or skipped.
|
|
1044
|
+
# We need to reset it to pilot.
|
|
1045
|
+
vobj.reset_to_pilot
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# Repair this title, and optionally all of its versions.
|
|
1049
|
+
#
|
|
1050
|
+
# Look at the Title Editor title object, and ensure it's correct based on the local data file.
|
|
1051
|
+
# - display name
|
|
1052
|
+
# - publisher
|
|
1053
|
+
# - EA or app-data
|
|
1054
|
+
# - ea name 'xolo-<title>'
|
|
1055
|
+
# - requirement criteria
|
|
1056
|
+
# - stub version if needed
|
|
1057
|
+
# - enabled
|
|
1058
|
+
#
|
|
1059
|
+
# Then look at the various Jamf objects pertaining to this title, and ensure they are correct
|
|
1060
|
+
# - Accept Patch EA
|
|
1061
|
+
# - Normal EA 'xolo-<title>-installed-version'
|
|
1062
|
+
# - title-installed smart group 'xolo-<title>-installed'
|
|
1063
|
+
# - frozen static group 'xolo-<title>-frozen'
|
|
1064
|
+
# - manual/SSvc install-current-release policy 'xolo-<title>-install'
|
|
1065
|
+
# - trigger 'xolo-<title>-install'
|
|
1066
|
+
# - ssvc icon
|
|
1067
|
+
# - ssvc category
|
|
1068
|
+
# - description
|
|
1069
|
+
# - if uninstallable
|
|
1070
|
+
# - uninstall script 'xolo-<title>-uninstall'
|
|
1071
|
+
# - uninstall policy 'xolo-<title>-uninstall'
|
|
1072
|
+
# - if expirable
|
|
1073
|
+
# - expire policy 'xolo-<title>-expire'
|
|
1074
|
+
# - trigger 'xolo-<title>-expire'
|
|
1075
|
+
#
|
|
1076
|
+
# @param repair_versions [Boolean] run the repair method on all versions?
|
|
1077
|
+
# @return [void]
|
|
1078
|
+
##################################
|
|
1079
|
+
def repair(repair_versions: false)
|
|
1080
|
+
lock
|
|
1081
|
+
@current_action = :repairing
|
|
1082
|
+
chg_log_msg = repair_versions ? 'Repairing title and all versions' : 'Repairing title only'
|
|
1083
|
+
log_change msg: chg_log_msg
|
|
1084
|
+
|
|
1085
|
+
progress "Starting repair of title '#{title}'"
|
|
1086
|
+
repair_ted_title
|
|
1087
|
+
repair_jamf_title_objects
|
|
1088
|
+
return unless repair_versions
|
|
1089
|
+
|
|
1090
|
+
version_objects.each do |vobj|
|
|
1091
|
+
progress '#########'
|
|
1092
|
+
vobj.repair
|
|
1093
|
+
end
|
|
1094
|
+
ensure
|
|
1095
|
+
unlock
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
# Is this title locked for updates?
|
|
1099
|
+
#############################
|
|
1100
|
+
def locked?
|
|
1101
|
+
self.class.locked?(title)
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
# Lock this title for updates
|
|
1105
|
+
#############################
|
|
1106
|
+
def lock
|
|
1107
|
+
raise Xolo::ServerError, 'Server is shutting down' if Xolo::Server.shutting_down?
|
|
1108
|
+
|
|
1109
|
+
while locked?
|
|
1110
|
+
log_debug "Waiting for update lock on title '#{title}'..." if (Time.now.to_i % 5).zero?
|
|
1111
|
+
sleep 0.33
|
|
1112
|
+
end
|
|
1113
|
+
Xolo::Server.object_locks[title] ||= { versions: {} }
|
|
1114
|
+
|
|
1115
|
+
exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
|
|
1116
|
+
Xolo::Server.object_locks[title][:expires] = exp
|
|
1117
|
+
log_debug "Locked title '#{title}' for updates until #{exp}"
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
# Unlock this v for updates
|
|
1121
|
+
#############################
|
|
1122
|
+
def unlock
|
|
1123
|
+
curr_lock = Xolo::Server.object_locks.dig title, :expires
|
|
1124
|
+
return unless curr_lock
|
|
1125
|
+
|
|
1126
|
+
Xolo::Server.object_locks[title].delete :expires
|
|
1127
|
+
log_debug "Unlocked title '#{title}' for updates"
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
# Add more server-specific data to our hash
|
|
1131
|
+
###########################
|
|
1132
|
+
def to_h
|
|
1133
|
+
hash = super
|
|
1134
|
+
hash[:ted_id_number] = ted_id_number
|
|
1135
|
+
hash[:ssvc_icon_id] = ssvc_icon_id
|
|
1136
|
+
hash
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
end # class Title
|
|
1140
|
+
|
|
1141
|
+
end # module Admin
|
|
1142
|
+
|
|
1143
|
+
end # module Xolo
|