xolo-server 1.0.0 → 2.0.2
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 +4 -4
- data/README.md +42 -4
- data/bin/xoloserver +3 -0
- data/data/client/xolo +1233 -0
- data/lib/optimist_with_insert_blanks.rb +1216 -0
- data/lib/xolo/core/base_classes/configuration.rb +238 -0
- data/lib/xolo/core/base_classes/server_object.rb +112 -0
- data/lib/xolo/core/base_classes/title.rb +884 -0
- data/lib/xolo/core/base_classes/version.rb +641 -0
- data/lib/xolo/core/constants.rb +85 -0
- data/lib/xolo/core/exceptions.rb +52 -0
- data/lib/xolo/core/json_wrappers.rb +43 -0
- data/lib/xolo/core/loading.rb +59 -0
- data/lib/xolo/core/output.rb +292 -0
- data/lib/xolo/core/security_cmd.rb +128 -0
- data/lib/xolo/core/version.rb +21 -0
- data/lib/xolo/core.rb +47 -0
- data/lib/xolo/server/app.rb +7 -0
- data/lib/xolo/server/configuration.rb +243 -38
- data/lib/xolo/server/constants.rb +10 -0
- data/lib/xolo/server/helpers/auth.rb +19 -2
- data/lib/xolo/server/helpers/autopkg.rb +157 -0
- data/lib/xolo/server/helpers/client_data.rb +90 -60
- data/lib/xolo/server/helpers/file_transfers.rb +412 -82
- data/lib/xolo/server/helpers/jamf_pro.rb +31 -7
- data/lib/xolo/server/helpers/log.rb +2 -0
- data/lib/xolo/server/helpers/maintenance.rb +1 -0
- data/lib/xolo/server/helpers/notification.rb +4 -3
- data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
- data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
- data/lib/xolo/server/helpers/subscriptions.rb +119 -0
- data/lib/xolo/server/helpers/titles.rb +27 -3
- data/lib/xolo/server/helpers/versions.rb +23 -11
- data/lib/xolo/server/mixins/changelog.rb +9 -16
- data/lib/xolo/server/mixins/title_jamf_access.rb +375 -390
- data/lib/xolo/server/mixins/title_ted_access.rb +50 -8
- data/lib/xolo/server/mixins/version_jamf_access.rb +118 -129
- data/lib/xolo/server/mixins/version_ted_access.rb +34 -4
- data/lib/xolo/server/object_locks.rb +2 -1
- data/lib/xolo/server/routes/auth.rb +2 -2
- data/lib/xolo/server/routes/jamf_pro.rb +11 -1
- data/lib/xolo/server/routes/maint.rb +2 -1
- data/lib/xolo/server/routes/subscriptions.rb +126 -0
- data/lib/xolo/server/routes/title_editor.rb +1 -1
- data/lib/xolo/server/routes/titles.rb +26 -11
- data/lib/xolo/server/routes/uploads.rb +0 -14
- data/lib/xolo/server/routes/versions.rb +14 -13
- data/lib/xolo/server/routes.rb +15 -23
- data/lib/xolo/server/title.rb +100 -77
- data/lib/xolo/server/version.rb +178 -18
- data/lib/xolo/server.rb +8 -0
- metadata +20 -11
|
@@ -20,6 +20,17 @@ module Xolo
|
|
|
20
20
|
#######################
|
|
21
21
|
#######################
|
|
22
22
|
|
|
23
|
+
UPLOAD_ACTION = 'Upload'
|
|
24
|
+
REUPLOAD_ACTION = 'Re-upload'
|
|
25
|
+
|
|
26
|
+
# Thes values in the cdnType field of the Cloud DP definition
|
|
27
|
+
# from the API indicate that there is no Cloud DP configured
|
|
28
|
+
CLOUD_DP_NA = [nil, Xolo::BLANK, 'NONE'].freeze
|
|
29
|
+
|
|
30
|
+
# How long to wait for a pkg to appear on the Cloud DP when uploading via API
|
|
31
|
+
# before giving up and raising an error
|
|
32
|
+
CLOUD_DP_UPLOAD_TIMEOUT = 1800 # seconds - 30 minutes
|
|
33
|
+
|
|
23
34
|
# Module Methods
|
|
24
35
|
#######################
|
|
25
36
|
#######################
|
|
@@ -60,7 +71,9 @@ module Xolo
|
|
|
60
71
|
log_info "Processing uploaded SelfService icon for #{params[:title]}"
|
|
61
72
|
title = instantiate_title params[:title]
|
|
62
73
|
title.save_ssvc_icon(tempfile, filename)
|
|
63
|
-
|
|
74
|
+
# this will configure the ssvc settings but won't make it available
|
|
75
|
+
# in ssvc - that happens elsewhere.
|
|
76
|
+
title.configure_pol_for_self_service
|
|
64
77
|
rescue => e
|
|
65
78
|
msg = "#{e.class}: #{e}"
|
|
66
79
|
log_error msg
|
|
@@ -69,81 +82,211 @@ module Xolo
|
|
|
69
82
|
halt 400, { status: 400, error: msg }
|
|
70
83
|
end
|
|
71
84
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
# Upload an pkg installer from xadm to Jamf Pro,
|
|
86
|
+
# and do all the processing around that
|
|
87
|
+
######################
|
|
88
|
+
def process_and_upload_uploaded_pkg
|
|
89
|
+
process_and_upload_to_jamf(
|
|
90
|
+
params[:title],
|
|
91
|
+
params[:version],
|
|
92
|
+
pkg_src: params[:file][:tempfile].path,
|
|
93
|
+
orig_filename: params[:file][:filename]
|
|
94
|
+
)
|
|
95
|
+
end
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
# upload a package from autopkg to Jamf Pro
|
|
98
|
+
# and do all the processing around that
|
|
99
|
+
#
|
|
100
|
+
# @param title [String] The title the package belongs to
|
|
101
|
+
# @param version [String, Xolo::Server::Version] the version string or object
|
|
102
|
+
# @param pkg_src [String, Pathname] the path to the pkg
|
|
103
|
+
# @return [void]
|
|
104
|
+
###########################################
|
|
105
|
+
def process_and_upload_autopkg_pkg(title, version, pkg_src)
|
|
106
|
+
process_and_upload_to_jamf(
|
|
107
|
+
title,
|
|
108
|
+
version,
|
|
109
|
+
pkg_src: pkg_src
|
|
110
|
+
)
|
|
111
|
+
end
|
|
83
112
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
# Process a pkg installer and upload to jamf
|
|
114
|
+
#
|
|
115
|
+
# @param title [String] the title name
|
|
116
|
+
# @param version [String, Xolo::Server::Version] the version string or object
|
|
117
|
+
# @param pkg_src [String, Pathname] the path to the file to be uploaded to Jamf
|
|
118
|
+
# This could be one uploaded from xadm, or one created by autopkg
|
|
119
|
+
# @param orig_filename [String, nil] the original filename of the pkg, e.g. as uploaded from
|
|
120
|
+
# an admin's computer. If not provided, the basename of pkg_src will be used.
|
|
121
|
+
# @return [void]
|
|
122
|
+
#############################
|
|
123
|
+
def process_and_upload_to_jamf(title, version, pkg_src:, orig_filename: nil)
|
|
124
|
+
pkg_src = Pathname.new pkg_src
|
|
125
|
+
orig_filename ||= pkg_src.basename.to_s
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
orig_filename = params[:file][:filename]
|
|
96
|
-
log_debug "Incoming pkg file '#{orig_filename}' "
|
|
97
|
-
file_extname = validate_uploaded_pkg(orig_filename)
|
|
127
|
+
version = instantiate_version(title: title, version: version) if version.is_a?(String)
|
|
98
128
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
staged_pkg = prep_pkg_for_upload(version, pkg_src)
|
|
130
|
+
upload_pkg_in_thread(version, staged_pkg, orig_filename)
|
|
131
|
+
rescue => e
|
|
132
|
+
msg = "#{e.class}: #{e}"
|
|
133
|
+
log_error msg
|
|
134
|
+
e.backtrace.each { |line| log_error "..#{line}" }
|
|
135
|
+
halt 400, { status: 400, error: msg }
|
|
136
|
+
ensure
|
|
137
|
+
pkg_src.delete if pkg_src.file?
|
|
138
|
+
end
|
|
103
139
|
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
# Prep a .pkg before we start uploading to the dist point
|
|
141
|
+
#
|
|
142
|
+
# @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
|
|
143
|
+
# @param pkg_src [Pathname] the path to the file to be uploaded to Jamf
|
|
144
|
+
# @return [Pathname] the path to the staged pkg that is ready to be uploaded to Jamf
|
|
145
|
+
#########################################
|
|
146
|
+
def prep_pkg_for_upload(version, pkg_src)
|
|
147
|
+
msg = "Jamf: Processing installer package '#{pkg_src}' (#{pkg_src.size.pix_humanize_bytes}) for Jamf Dist upload, title '#{version.title}' version '#{version.version}'"
|
|
148
|
+
progress msg, log: :info
|
|
149
|
+
|
|
150
|
+
version.jamf_pkg_file = dist_pkg_filename(version)
|
|
151
|
+
log_debug "Jamf: Uploaded package filename will be '#{version.jamf_pkg_file}'"
|
|
152
|
+
version.save_local_data
|
|
106
153
|
|
|
107
|
-
# The
|
|
108
|
-
|
|
109
|
-
staged_pkg = Xolo::Server::Title.title_dir(params[:title]) + uploaded_pkg_name
|
|
154
|
+
# The pkg_src will be staged here before uploading to the Dist Point
|
|
155
|
+
staged_pkg = Xolo::Server::Title.title_dir(version.title) + version.jamf_pkg_file
|
|
110
156
|
|
|
111
157
|
# remove any old one that might be there
|
|
112
158
|
staged_pkg.delete if staged_pkg.file?
|
|
113
159
|
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log_debug "Signing complete, deleting temp file '#{tempfile}'"
|
|
118
|
-
tempfile.delete if tempfile.file?
|
|
119
|
-
else
|
|
120
|
-
log_debug "Uploaded .pkg file doesn't need signing, moving tempfile to '#{staged_pkg.basename}'"
|
|
121
|
-
# Put the signed pkg into the staged_pkg location
|
|
122
|
-
tempfile.rename staged_pkg
|
|
123
|
-
end
|
|
160
|
+
# This will move/copy the pkg_src into the staged_pkg, signing it on the way if needed, and
|
|
161
|
+
# delete the original pkg_src file.
|
|
162
|
+
sign_and_stage(pkg_src, staged_pkg, version)
|
|
124
163
|
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
upload_to_dist_point(version.jamf_package, staged_pkg)
|
|
164
|
+
# Wrap component pkgs in a Distribution pkg if configured to do so
|
|
165
|
+
staged_pkg = wrap_component_pkg_in_distribution(staged_pkg, version) if Xolo::Server.config.create_distribution_pkgs
|
|
128
166
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
version.reupload_date = Time.now
|
|
132
|
-
version.reuploaded_by = session[:admin]
|
|
167
|
+
staged_pkg
|
|
168
|
+
end
|
|
133
169
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
# upload a prepped/staged pkg in a thread, and do the things that need to be done after upload,
|
|
171
|
+
# like setting the upload/reupload date and user, enabling the reinstall policy if it's a reupload, etc
|
|
172
|
+
#
|
|
173
|
+
# @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
|
|
174
|
+
# @param staged_pkg [Pathname] the path to the staged pkg that is ready to be uploaded to Jamf
|
|
175
|
+
# @return [void]
|
|
176
|
+
#####################################
|
|
177
|
+
def upload_pkg_in_thread(version, staged_pkg, orig_filename)
|
|
178
|
+
if @pkg_upload_thread&.alive?
|
|
179
|
+
msg = "A pkg upload is already in progress for version '#{version}' - can't start another one until it's done"
|
|
180
|
+
log_error msg
|
|
181
|
+
raise msg
|
|
141
182
|
end
|
|
142
183
|
|
|
184
|
+
@pkg_upload_thread = Thread.new do
|
|
185
|
+
begin
|
|
186
|
+
# is this a re-upload? The jamf_pkg_file will have already
|
|
187
|
+
# been updated to reflect the new filename with the _N_ if it's a re-upload
|
|
188
|
+
re_uploading = version.jamf_pkg_file =~ /_(\d+)_\.pkg$/
|
|
189
|
+
|
|
190
|
+
# disable reinstall policy if re-uploading,
|
|
191
|
+
# will be re-enabled after the upload
|
|
192
|
+
if re_uploading
|
|
193
|
+
action = REUPLOAD_ACTION
|
|
194
|
+
pol = version.jamf_auto_reinstall_policy
|
|
195
|
+
pol.disable
|
|
196
|
+
pol.save
|
|
197
|
+
else
|
|
198
|
+
action = UPLOAD_ACTION
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
upload_to_dist_point(version.jamf_package, staged_pkg)
|
|
202
|
+
|
|
203
|
+
uploaded_by =
|
|
204
|
+
if version.title_object.autopkg_enabled?
|
|
205
|
+
Xolo::Server::Helpers::AutoPkg::AUTOPKG_UPLOADED_BY
|
|
206
|
+
elsif defined?(session)
|
|
207
|
+
session[:admin]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if re_uploading
|
|
211
|
+
version.reupload_date = Time.now
|
|
212
|
+
version.reuploaded_by = uploaded_by
|
|
213
|
+
|
|
214
|
+
# if upload via API, wait for pkg to appear then enable the policy
|
|
215
|
+
if upload_via_api?
|
|
216
|
+
wait_for_pkg_and_enable_reinstall_policy(version)
|
|
217
|
+
|
|
218
|
+
# otherwise notify someone to confirm upload is complete before enabling the policy
|
|
219
|
+
else
|
|
220
|
+
msg = "Please confirm that re-uploaded pkg '#{version.jamf_pkg_file}' is on the dist point and ready to go, then enable the reinstall policy '#{pol.name}' at #{jamf_auto_reinstall_policy_url}"
|
|
221
|
+
log_info msg, alert: true
|
|
222
|
+
|
|
223
|
+
end # if upload_via_api?
|
|
224
|
+
|
|
225
|
+
# update the dist filename in the jamf package object
|
|
226
|
+
version.jamf_package.fileName = version.jamf_pkg_file
|
|
227
|
+
version.jamf_package.packageName = version.jamf_pkg_name
|
|
228
|
+
version.jamf_package.save
|
|
229
|
+
|
|
230
|
+
# if this is a first-time upload, just set the upload date and user
|
|
231
|
+
else
|
|
232
|
+
version.upload_date = Time.now
|
|
233
|
+
version.uploaded_by = uploaded_by
|
|
234
|
+
end # if re_uploading
|
|
235
|
+
version.save_local_data
|
|
236
|
+
version.log_change msg: "#{action}ed pkg file '#{staged_pkg.basename}' to Jamf Pro dist point(s)"
|
|
237
|
+
rescue => e
|
|
238
|
+
msg = "Error in pkg upload thread: #{e.class}: #{e}"
|
|
239
|
+
log_error msg
|
|
240
|
+
e.backtrace.each { |line| log_error "..#{line}" }
|
|
241
|
+
end # begin
|
|
242
|
+
|
|
243
|
+
update_version_post_upload(version, staged_pkg, orig_filename)
|
|
244
|
+
end # thread
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Wait for a re-uploaded pkg to appear on the Cloud DP after an API upload,
|
|
248
|
+
# then enable the reinstall policy
|
|
249
|
+
#
|
|
250
|
+
# @param version [Xolo::Server::Version] the version that is being re-uploaded
|
|
251
|
+
# @return [void]
|
|
252
|
+
#####################
|
|
253
|
+
def wait_for_pkg_and_enable_reinstall_policy(version)
|
|
254
|
+
start_time = Time.now
|
|
255
|
+
|
|
256
|
+
until cloud_dp_pkg_ready?(version.jamf_pkg_file)
|
|
257
|
+
if Time.now - start_time > CLOUD_DP_UPLOAD_TIMEOUT
|
|
258
|
+
msg = "Timed out waiting for pkg '#{version.jamf_pkg_file}' to appear on Cloud DP after upload via API"
|
|
259
|
+
log_error msg
|
|
260
|
+
raise msg
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
log_debug "Checking every minute for pkg '#{version.jamf_pkg_file}' to appear on Cloud DP after upload via API..."
|
|
264
|
+
sleep 60
|
|
265
|
+
end # until
|
|
266
|
+
|
|
267
|
+
log_debug "Pkg '#{version.jamf_pkg_file}' is now on Cloud DP, enabling reinstall policy"
|
|
268
|
+
pol = version.jamf_auto_reinstall_policy
|
|
269
|
+
pol.enable
|
|
270
|
+
pol.save
|
|
271
|
+
|
|
272
|
+
msg = "Re-uploaded pkg '#{version.jamf_pkg_file}' is on the dist point and ready to go, reinstall policy '#{pol.name}' has been enabled"
|
|
273
|
+
log_info msg, alert: true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# After uploading a pkg, update the version with info about the pkg,
|
|
277
|
+
# like whether it's a dist pkg or not, and save the manifest and checksum
|
|
278
|
+
# NOTE this is run as part of the upload thread.
|
|
279
|
+
#
|
|
280
|
+
# @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
|
|
281
|
+
# @param staged_pkg [Pathname] the path to the staged pkg that was uploaded to Jamf
|
|
282
|
+
# @return [void]
|
|
283
|
+
##############################
|
|
284
|
+
def update_version_post_upload(version, staged_pkg, orig_filename)
|
|
143
285
|
# make note if the pkg is a Distribution package
|
|
144
286
|
version.dist_pkg = pkg_is_distribution?(staged_pkg)
|
|
145
287
|
|
|
146
|
-
# save the manifest just in case
|
|
288
|
+
# save the manifest on the server, just in case
|
|
289
|
+
# TODO: Support sha3_512 in manifests
|
|
147
290
|
version.manifest_file.pix_atomic_write(version.jamf_package.manifest)
|
|
148
291
|
|
|
149
292
|
# save the checksum just in case
|
|
@@ -156,18 +299,59 @@ module Xolo
|
|
|
156
299
|
version.save_local_data
|
|
157
300
|
|
|
158
301
|
# log the upload
|
|
159
|
-
version.log_change msg: "
|
|
160
|
-
|
|
161
|
-
# remove the staged pkg and the tempfile
|
|
162
|
-
staged_pkg.delete
|
|
163
|
-
tempfile.delete if tempfile.file?
|
|
164
|
-
rescue => e
|
|
165
|
-
msg = "#{e.class}: #{e}"
|
|
166
|
-
log_error msg
|
|
167
|
-
e.backtrace.each { |line| log_error "..#{line}" }
|
|
168
|
-
halt 400, { status: 400, error: msg }
|
|
302
|
+
version.log_change msg: "Uploaded pkg file '#{staged_pkg.basename}' to dist point"
|
|
169
303
|
ensure
|
|
170
|
-
|
|
304
|
+
staged_pkg.delete if staged_pkg.file?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# What will be the name of the file on the dist point?
|
|
308
|
+
# For a first upload, it will be 'xolo-<title>-<version>.pkg'
|
|
309
|
+
#
|
|
310
|
+
# If we are re-uploading, it will be 'xolo-<title>-<version>_N_.pkg'
|
|
311
|
+
# where N is an integer (starting with 2) that increments with each re-upload,
|
|
312
|
+
#
|
|
313
|
+
# This is so that re-uploads don't have the same filename on the Dist Point, which could
|
|
314
|
+
# be problematic. It also helps to visually see that this is a re-uploaded pkg
|
|
315
|
+
# and not the original one.
|
|
316
|
+
#
|
|
317
|
+
# @param version [Xolo::Server::Version] the version that is being re-uploaded
|
|
318
|
+
#
|
|
319
|
+
# @return [String] the filename to use for the pkg on the dist point
|
|
320
|
+
####################
|
|
321
|
+
def dist_pkg_filename(version)
|
|
322
|
+
if version.upload_date.pix_empty?
|
|
323
|
+
# no upload date, this is the first upload
|
|
324
|
+
"#{version.jamf_pkg_name}#{Xolo::DOT_PKG}"
|
|
325
|
+
|
|
326
|
+
elsif version.jamf_pkg_file.to_s =~ /_(\d+)_\.pkg$/
|
|
327
|
+
# this is a re-upload, does the filename indicat a previous re-upload with a _N_ in the name?
|
|
328
|
+
next_num = Regexp.last_match[1].to_i + 1
|
|
329
|
+
"#{version.jamf_pkg_name}_#{next_num}_#{Xolo::DOT_PKG}"
|
|
330
|
+
|
|
331
|
+
else
|
|
332
|
+
# the first re-upload, just add _2_ before the extension
|
|
333
|
+
"#{version.jamf_pkg_name}_2_#{Xolo::DOT_PKG}"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# If this pkg needs signing, do so putting the signed pkg in the staged_pkg location,
|
|
338
|
+
# and delete the original pkg_src file.
|
|
339
|
+
# If it doesn't need signing, just move it to the staged_pkg location.
|
|
340
|
+
#
|
|
341
|
+
# @param pkg_src [Pathname] the path to the file to be uploaded to Jamf
|
|
342
|
+
# @param staged_pkg [Pathname] the path where the pkg should be staged for upload to Jamf
|
|
343
|
+
# @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
|
|
344
|
+
# @return [void]
|
|
345
|
+
def sign_and_stage(pkg_src, staged_pkg, version)
|
|
346
|
+
if need_to_sign?(pkg_src, version)
|
|
347
|
+
# This will put the signed pkg into the staged_pkg location
|
|
348
|
+
sign_pkg(pkg_src, staged_pkg)
|
|
349
|
+
log_debug "Signing complete, signed pkg is '#{staged_pkg}', deleting original file '#{pkg_src}'"
|
|
350
|
+
pkg_src.delete if pkg_src.file?
|
|
351
|
+
else
|
|
352
|
+
log_debug "The .pkg file doesn't need signing, moving pkg_src to '#{staged_pkg}'"
|
|
353
|
+
pkg_src.rename staged_pkg
|
|
354
|
+
end
|
|
171
355
|
end
|
|
172
356
|
|
|
173
357
|
# Check if a package is a Distribution package, if not,
|
|
@@ -182,14 +366,69 @@ module Xolo
|
|
|
182
366
|
pkg_file = Pathname.new(pkg_file)
|
|
183
367
|
raise ArgumentError, "pkg_file does not exist or not a file: #{pkg_file}" unless pkg_file.file?
|
|
184
368
|
|
|
185
|
-
|
|
186
|
-
|
|
369
|
+
`/usr/bin/xar -tf #{pkg_file.to_s.shellescape}`.split("\n").include? 'Distribution'
|
|
370
|
+
end
|
|
187
371
|
|
|
188
|
-
|
|
372
|
+
# Wrap a component pkg in a Distribution pkg, return the path to the Distribution pkg,
|
|
373
|
+
# which should be the same as the orig_pkg
|
|
374
|
+
#
|
|
375
|
+
# @param orig_pkg [Pathname, String] The path to the component .pkg
|
|
376
|
+
# @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
|
|
377
|
+
#
|
|
378
|
+
# @return [Pathname] The path to the new Distribution pkg
|
|
379
|
+
###########################################
|
|
380
|
+
def wrap_component_pkg_in_distribution(orig_pkg, version)
|
|
381
|
+
orig_pkg = Pathname.new(orig_pkg)
|
|
189
382
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
383
|
+
raise ArgumentError, "pkg_file does not exist or not a file: #{orig_pkg}" unless orig_pkg.file?
|
|
384
|
+
|
|
385
|
+
if pkg_is_distribution?(orig_pkg)
|
|
386
|
+
log_debug "Package '#{orig_pkg.basename}' is already a Distribution pkg, not wrapping"
|
|
387
|
+
return orig_pkg
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
log_info "Wrapping component pkg '#{orig_pkg.basename}' in a Distribution pkg"
|
|
391
|
+
out_dir = orig_pkg.parent
|
|
392
|
+
out_file = out_dir + "#{orig_pkg.basename(Xolo::DOT_PKG)}_dist#{Xolo::DOT_PKG}"
|
|
393
|
+
|
|
394
|
+
# the productbuild command, with signing if needed
|
|
395
|
+
prodbuild_cmd = +"/usr/bin/productbuild --package #{orig_pkg.to_s.shellescape} "
|
|
396
|
+
signing_reason =
|
|
397
|
+
if version.pkg_is_from_autopkg && Xolo::Server.config.sign_autopkg_pkgs
|
|
398
|
+
'autopkg'
|
|
399
|
+
elsif !version.pkg_is_from_autopkg && Xolo::Server.config.sign_pkgs
|
|
400
|
+
'uploaded'
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
if signing_reason
|
|
404
|
+
log_info "Signing is enabled for #{signing_reason} pkgs, will sign the Distribution pkg as part of wrapping process"
|
|
405
|
+
|
|
406
|
+
sh_kch = Shellwords.escape Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
|
|
407
|
+
sh_ident = Shellwords.escape Xolo::Server.config.pkg_signing_identity
|
|
408
|
+
unlock_signing_keychain
|
|
409
|
+
prodbuild_cmd << "--sign #{sh_ident} --keychain #{sh_kch} "
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
prodbuild_cmd << out_file.to_s.shellescape
|
|
413
|
+
|
|
414
|
+
log_debug "Wrapping component pkg in Distribution pkg with this command: #{prodbuild_cmd}"
|
|
415
|
+
|
|
416
|
+
if system prodbuild_cmd
|
|
417
|
+
# remove the component pkg
|
|
418
|
+
orig_pkg.delete
|
|
419
|
+
# rename the dist pkg to the original pkg name,
|
|
420
|
+
out_file.rename orig_pkg
|
|
421
|
+
|
|
422
|
+
return orig_pkg
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
raise "Failed to wrap component pkg '#{orig_pkg.basename}' in a Distribution pkg"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# are Dist Point uploads configured to be done via the API, or with an upload tool defined in config?
|
|
429
|
+
#############################
|
|
430
|
+
def upload_via_api?
|
|
431
|
+
Xolo::Server.config.upload_tool.to_s.downcase == 'api'
|
|
193
432
|
end
|
|
194
433
|
|
|
195
434
|
# upload a staged pkg to the dist point(s)
|
|
@@ -201,9 +440,17 @@ module Xolo
|
|
|
201
440
|
# @return [void]
|
|
202
441
|
###########################################
|
|
203
442
|
def upload_to_dist_point(jpkg, pkg_file)
|
|
204
|
-
|
|
205
|
-
|
|
443
|
+
# via API
|
|
444
|
+
if upload_via_api?
|
|
445
|
+
log_debug 'Jamf: increasing the API timeout to 30 minutes for the pkg upload'
|
|
446
|
+
jpkg.cnx.jp_cnx.options.timeout = 1800
|
|
447
|
+
|
|
448
|
+
log_debug "Jamf: Attempting upload of #{pkg_file.basename} to primary dist point via API"
|
|
449
|
+
# this will update the checksum and manifest automatically, and save back to the jamf pro server
|
|
450
|
+
jpkg.upload pkg_file
|
|
206
451
|
log_info "Jamf: Uploaded #{pkg_file.basename} to primary dist point via API, with new checksum and manifest"
|
|
452
|
+
|
|
453
|
+
# via upload tool defined in config
|
|
207
454
|
else
|
|
208
455
|
log_debug "Jamf: Regenerating manifest for package '#{jpkg.packageName}' from #{pkg_file.basename}"
|
|
209
456
|
jpkg.generate_manifest(pkg_file)
|
|
@@ -233,19 +480,22 @@ module Xolo
|
|
|
233
480
|
cmd = "#{tool} #{jpkg_name} #{pkg}"
|
|
234
481
|
|
|
235
482
|
stdouterr, exit_status = Open3.capture2e(cmd)
|
|
236
|
-
|
|
483
|
+
if exit_status.success?
|
|
484
|
+
log_debug "Jamf: upload tool succeeded in uploading #{pkg_file.basename} to dist point(s)."
|
|
485
|
+
return
|
|
486
|
+
end
|
|
237
487
|
|
|
238
|
-
msg =
|
|
488
|
+
msg = "Uploader tool failed to upload #{pkg_file.basename} to dist point(s): #{stdouterr}"
|
|
239
489
|
log_error msg
|
|
240
490
|
raise msg
|
|
241
491
|
end
|
|
242
492
|
|
|
243
493
|
# Confirm and return the extension of the originally uplaoded file,
|
|
244
|
-
#
|
|
494
|
+
# as .pkg
|
|
245
495
|
#
|
|
246
496
|
# @param filename [String] The original name of the file uploaded to Xolo.
|
|
247
497
|
#
|
|
248
|
-
# @return [String]
|
|
498
|
+
# @return [String] '.pkg' is the only valid one for now
|
|
249
499
|
###############################
|
|
250
500
|
def validate_uploaded_pkg(filename)
|
|
251
501
|
log_debug "Validating pkg file ext for '#{filename}'"
|
|
@@ -253,7 +503,87 @@ module Xolo
|
|
|
253
503
|
file_extname = Pathname.new(filename).extname
|
|
254
504
|
return file_extname if Xolo::OK_PKG_EXTS.include? file_extname
|
|
255
505
|
|
|
256
|
-
raise "Bad filename '#{filename}'. Package files must end in .
|
|
506
|
+
raise "Bad filename '#{filename}'. Package files must end in #{Xolo::OK_PKG_EXTS.join(', or ')}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# TODO: Use ruby-jss when it implements could-distribution-point rsrc
|
|
510
|
+
#
|
|
511
|
+
# @return [Hash] The Cloud DP definition from the API, if available, minus the keyPairId & privateKey
|
|
512
|
+
# If no cloud dp defined, returns { cdnType: 'NONE', master: false }
|
|
513
|
+
####################
|
|
514
|
+
def cloud_dp_data
|
|
515
|
+
return @cloud_dp_data if @cloud_dp_data
|
|
516
|
+
|
|
517
|
+
@cloud_dp_data = jamf_cnx.jp_get '/v1/cloud-distribution-point'
|
|
518
|
+
@cloud_dp_data.delete :privateKey
|
|
519
|
+
@cloud_dp_data.delete :keyPairId
|
|
520
|
+
@cloud_dp_data
|
|
521
|
+
rescue Jamf::Connection::JamfProAPIError => e
|
|
522
|
+
@cloud_dp_data = { cdnType: 'NONE', master: false }
|
|
523
|
+
return @cloud_dp_data if jamf_cnx.last_http_response.status == 404
|
|
524
|
+
|
|
525
|
+
raise e
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# @return [Boolean] Is a cloud distribution point defined?
|
|
529
|
+
###############################
|
|
530
|
+
def cloud_dp_available?
|
|
531
|
+
!CLOUD_DP_NA.include? cloud_dp_data[:cdnType]
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# TODO: Use ruby-jss when it implements could-distribution-point rsrc
|
|
535
|
+
# @return [Boolean] Is a cloud distribution point defined?
|
|
536
|
+
###############################
|
|
537
|
+
def cloud_dp_principal?
|
|
538
|
+
cloud_dp_available? && cloud_dp_data[:master]
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# TODO: Use ruby-jss when it implements could-distribution-point rsrc
|
|
542
|
+
#
|
|
543
|
+
# Does a given pkg name exist on the cloud dp with 'ready' status?
|
|
544
|
+
#
|
|
545
|
+
# @param pkg_name [String] the name of the pkg to look for
|
|
546
|
+
#
|
|
547
|
+
# @return [Boolean] Is the pkg ready-to-go on the Cloud DP?
|
|
548
|
+
###############################
|
|
549
|
+
def cloud_dp_pkg_ready?(pkg_filename)
|
|
550
|
+
return false unless cloud_dp_available?
|
|
551
|
+
|
|
552
|
+
filt = CGI.escape "fileName=='#{pkg_filename}'"
|
|
553
|
+
response = jamf_cnx.jp_get "/v1/cloud-distribution-point/files?filter=#{filt}"
|
|
554
|
+
|
|
555
|
+
# No fileserver I know of will allow multiples of a single filename....
|
|
556
|
+
# so assume there's only zero or one
|
|
557
|
+
data = response[:results].first
|
|
558
|
+
return false unless data
|
|
559
|
+
|
|
560
|
+
# once the status is ready, we should be good to go
|
|
561
|
+
data[:status] == 'READY'
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# TODO: Use ruby-jss when it implements could-distribution-point rsrc
|
|
565
|
+
#
|
|
566
|
+
# @return [Hash {String => String}] The Jamf ID => FileName of all 'READY' PACKAGE files on
|
|
567
|
+
# the Cloud DP.
|
|
568
|
+
###############################
|
|
569
|
+
def cloud_dp_pkgs
|
|
570
|
+
page = 0
|
|
571
|
+
page_size = 1000
|
|
572
|
+
pkgs = {}
|
|
573
|
+
loop do
|
|
574
|
+
response = jamf_cnx.jp_get "/v1/cloud-distribution-point/files?page=#{page}&page-size=#{page_size}"
|
|
575
|
+
results = response[:results]
|
|
576
|
+
break if results.empty?
|
|
577
|
+
|
|
578
|
+
results.each do |f|
|
|
579
|
+
next unless f[:type] == 'PACKAGE' && f[:status] == 'READY'
|
|
580
|
+
|
|
581
|
+
# fileObjectId is the Jamf ID of the Package object for this DP file
|
|
582
|
+
pkgs[f[:fileObjectId]] = f[:fileName]
|
|
583
|
+
end
|
|
584
|
+
page += 1
|
|
585
|
+
end
|
|
586
|
+
pkgs
|
|
257
587
|
end
|
|
258
588
|
|
|
259
589
|
end # FileTransfers
|
|
@@ -68,6 +68,7 @@ module Xolo
|
|
|
68
68
|
|
|
69
69
|
host = Xolo::Server.config.jamf_gui_hostname
|
|
70
70
|
host ||= Xolo::Server.config.jamf_hostname
|
|
71
|
+
|
|
71
72
|
port = Xolo::Server.config.jamf_gui_port
|
|
72
73
|
port ||= Xolo::Server.config.jamf_port
|
|
73
74
|
|
|
@@ -93,20 +94,30 @@ module Xolo
|
|
|
93
94
|
|
|
94
95
|
return @jamf_cnx if @jamf_cnx
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
cnx_opts = {
|
|
97
98
|
name: "jamf-pro-cnx-#{Time.now.strftime('%F-%T')}",
|
|
98
99
|
host: Xolo::Server.config.jamf_hostname,
|
|
99
100
|
port: Xolo::Server.config.jamf_port,
|
|
100
101
|
verify_cert: Xolo::Server.config.jamf_verify_cert,
|
|
101
102
|
ssl_version: Xolo::Server.config.jamf_ssl_version,
|
|
102
103
|
open_timeout: Xolo::Server.config.jamf_open_timeout,
|
|
103
|
-
timeout: Xolo::Server.config.jamf_timeout
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
timeout: Xolo::Server.config.jamf_timeout
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if Xolo::Server.config.jamf_use_api_client
|
|
108
|
+
cnxtype = 'API Client'
|
|
109
|
+
cnx_opts[:client_id] = Xolo::Server.config.jamf_api_user
|
|
110
|
+
cnx_opts[:client_secret] = Xolo::Server.config.jamf_api_pw
|
|
111
|
+
else
|
|
112
|
+
cnxtype = 'User'
|
|
113
|
+
cnx_opts[:user] = Xolo::Server.config.jamf_api_user
|
|
114
|
+
cnx_opts[:pw] = Xolo::Server.config.jamf_api_pw
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@jamf_cnx = Jamf::Connection.new(**cnx_opts)
|
|
109
118
|
|
|
119
|
+
log_debug "Jamf: Connected to Jamf Pro at #{@jamf_cnx.base_url} as #{cnxtype} '#{Xolo::Server.config.jamf_api_user}'. KeepAlive: false, Expires: #{@jamf_cnx.token.expires}. cnx ID: #{@jamf_cnx.object_id}"
|
|
120
|
+
# log_debug "jamf_cnx caller:\n..#{caller_locations(1, 10).map(&:to_s).join("\n..")}"
|
|
110
121
|
@jamf_cnx
|
|
111
122
|
end
|
|
112
123
|
|
|
@@ -147,6 +158,19 @@ module Xolo
|
|
|
147
158
|
end
|
|
148
159
|
end
|
|
149
160
|
|
|
161
|
+
# If we're running on a test server, Jamf objects have a different prefix
|
|
162
|
+
# which is 'xolo-' prod servers and 'xolotest-' on test servers.
|
|
163
|
+
#
|
|
164
|
+
# @return [String] The prefix for Jamf object names
|
|
165
|
+
###########################
|
|
166
|
+
def jamf_obj_name_pfx_base
|
|
167
|
+
if Xolo::Server.config.test_server
|
|
168
|
+
Xolo::Server::JAMF_TEST_OBJECT_NAME_PFX
|
|
169
|
+
else
|
|
170
|
+
Xolo::Server::JAMF_OBJECT_NAME_PFX
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
150
174
|
end # JamfPro
|
|
151
175
|
|
|
152
176
|
end # Helpers
|
|
@@ -73,12 +73,14 @@ module Xolo
|
|
|
73
73
|
###############################
|
|
74
74
|
def log_error(msg, alert: false)
|
|
75
75
|
logger.error(session_svr_obj_id) { msg }
|
|
76
|
+
# caller.each { |l| logger.error(session_svr_obj_id) { "..#{l}" } }
|
|
76
77
|
send_alert msg, :ERROR if alert
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
###############################
|
|
80
81
|
def log_fatal(msg, alert: false)
|
|
81
82
|
logger.fatal(session_svr_obj_id) { msg }
|
|
83
|
+
caller.each { |l| logger.fatal(session_svr_obj_id) { "..#{l}" } }
|
|
82
84
|
send_alert msg, :FATAL if alert
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -175,6 +175,7 @@ module Xolo
|
|
|
175
175
|
# TODO: Be DRY with this stuff and similar in title_jamf_access.rb
|
|
176
176
|
Xolo::Server::Title.all_titles.each do |title|
|
|
177
177
|
title_obj = instantiate_title title
|
|
178
|
+
next if title_obj.subscribed?
|
|
178
179
|
next unless title_obj.jamf_patch_ea_awaiting_acceptance?
|
|
179
180
|
|
|
180
181
|
log_info "Cleanup: Auto-accepting Title Editor EA for title '#{title}'"
|