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,415 @@
|
|
|
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 Helpers
|
|
16
|
+
|
|
17
|
+
# Constants and methods for maintaining the client data package
|
|
18
|
+
#
|
|
19
|
+
# This is used as a 'helper' in the Sinatra server
|
|
20
|
+
#
|
|
21
|
+
# This means methods here are available in all routes, views, and helpers
|
|
22
|
+
# the Sinatra server app.
|
|
23
|
+
#
|
|
24
|
+
# The client data package is a Jamf::JPackage that installs a JSON file on all
|
|
25
|
+
# managed Macs. This JSON file contains data about all titles and versions, and
|
|
26
|
+
# any other data that the xolo client needs to know about.
|
|
27
|
+
#
|
|
28
|
+
# It is updated automatically by the server when titles or versions are changed.
|
|
29
|
+
#
|
|
30
|
+
# It is used so that the xolo client can know what it needs to know about titles and
|
|
31
|
+
# versions without having to query the server or do anything over a network other
|
|
32
|
+
# than using the jamf binary.
|
|
33
|
+
#
|
|
34
|
+
# The downside is that the client data package is likely to be somewhat out of date,
|
|
35
|
+
# but that is a tradeoff for the simplicity and security of the client.
|
|
36
|
+
#
|
|
37
|
+
# The client data package is installed in /Library/Application Support/xolo/client-data.json
|
|
38
|
+
# it contains a JSON object with a 'titles' key, which is an object with keys for each title.
|
|
39
|
+
# The data provided is that produced by the Title#to_h and Version#to_h methods.
|
|
40
|
+
module ClientData
|
|
41
|
+
|
|
42
|
+
# Constants
|
|
43
|
+
#
|
|
44
|
+
##############################
|
|
45
|
+
##############################
|
|
46
|
+
|
|
47
|
+
CLIENT_DATA_STR = 'client-data'
|
|
48
|
+
|
|
49
|
+
# The name of the Jamf Package object that contains the xolo-client-data
|
|
50
|
+
# NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
|
|
51
|
+
CLIENT_DATA_PACKAGE_NAME = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{CLIENT_DATA_STR}"
|
|
52
|
+
|
|
53
|
+
# The name of the package file that installs the xolo-client-data JSON file
|
|
54
|
+
CLIENT_DATA_COMPONENT_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}-component.pkg"
|
|
55
|
+
|
|
56
|
+
CLIENT_DATA_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}.pkg"
|
|
57
|
+
|
|
58
|
+
# The package identifier for the xolo-client-data package
|
|
59
|
+
CLIENT_DATA_PACKAGE_IDENTIFIER = "com.pixar.xolo.#{CLIENT_DATA_STR}"
|
|
60
|
+
|
|
61
|
+
# The name of the Jamf::Policy object that installs the xolo-client-data package
|
|
62
|
+
# automatically on all managed Macs
|
|
63
|
+
# NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
|
|
64
|
+
CLIENT_DATA_AUTO_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-auto"
|
|
65
|
+
|
|
66
|
+
# The name of the Jamf::Policy object that installs the xolo-client-data package
|
|
67
|
+
# manually on a managed Mac
|
|
68
|
+
CLIENT_DATA_MANUAL_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-manual"
|
|
69
|
+
|
|
70
|
+
# The name of the client-data JSON file in the xolo-client-data package
|
|
71
|
+
# this is the file that is installed onto managed Macs in
|
|
72
|
+
# /Library/Application Support/xolo/
|
|
73
|
+
CLIENT_DATA_FILE = 'client-data.json'
|
|
74
|
+
|
|
75
|
+
# The trgger event for the manual policy to update the client data JSON file
|
|
76
|
+
CLIENT_DATA_MANUAL_POLICY_TRIGGER = 'update-xolo-client-data'
|
|
77
|
+
|
|
78
|
+
# Module methods
|
|
79
|
+
#
|
|
80
|
+
# These are available as module methods but not as 'helper'
|
|
81
|
+
# methods in sinatra routes & views.
|
|
82
|
+
#
|
|
83
|
+
##############################
|
|
84
|
+
##############################
|
|
85
|
+
|
|
86
|
+
# when this module is included
|
|
87
|
+
##############################
|
|
88
|
+
def self.included(includer)
|
|
89
|
+
Xolo.verbose_include includer, self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# when this module is extended
|
|
93
|
+
def self.extended(extender)
|
|
94
|
+
Xolo.verbose_extend extender, self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A mutex for the client data update process
|
|
98
|
+
#
|
|
99
|
+
# TODO: use Concrrent Ruby instead of Mutex
|
|
100
|
+
#
|
|
101
|
+
# @return [Mutex] the mutex
|
|
102
|
+
#####################
|
|
103
|
+
def self.client_data_mutex
|
|
104
|
+
@client_data_mutex ||= Mutex.new
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Instance methods
|
|
108
|
+
#
|
|
109
|
+
# These are available directly in sinatra routes and views
|
|
110
|
+
#
|
|
111
|
+
##############################
|
|
112
|
+
##############################
|
|
113
|
+
|
|
114
|
+
# @return [Jamf::JPackage] the xolo-client-data package object
|
|
115
|
+
#####################
|
|
116
|
+
def client_data_jpackage
|
|
117
|
+
return @client_data_jpackage if @client_data_jpackage
|
|
118
|
+
|
|
119
|
+
@client_data_jpackage = Jamf::JPackage.fetch packageName: CLIENT_DATA_PACKAGE_NAME, cnx: jamf_cnx
|
|
120
|
+
rescue Jamf::NoSuchItemError
|
|
121
|
+
@client_data_jpackage = create_client_data_jamf_package
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# update the xolo-client-data package and the policy that installs it
|
|
125
|
+
#
|
|
126
|
+
# This package installs a JSON file with data about all titles and versions
|
|
127
|
+
# for use by the xolo client on managed Macs.
|
|
128
|
+
#
|
|
129
|
+
# This process is protected by a mutex to prevent multiple updates at the same time.
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
#####################
|
|
133
|
+
def update_client_data
|
|
134
|
+
# don't do anything if we are in developer/test mode
|
|
135
|
+
if Xolo::Server.config.developer_mode?
|
|
136
|
+
log_debug 'Jamf: Skipping client-data update in developer mode'
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
log_info 'Jamf: Updating client-data package'
|
|
141
|
+
|
|
142
|
+
# TODO: Use Concurrent Ruby instead of Mutex
|
|
143
|
+
mutex = Xolo::Server::Helpers::ClientData.client_data_mutex
|
|
144
|
+
|
|
145
|
+
until mutex.try_lock
|
|
146
|
+
progress 'Waiting for another client data update to finish', log: :info
|
|
147
|
+
sleep 5
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
new_pkg = create_new_client_data_pkg_file
|
|
151
|
+
upload_to_dist_point client_data_jpackage, new_pkg
|
|
152
|
+
|
|
153
|
+
create_client_data_policies_if_needed
|
|
154
|
+
|
|
155
|
+
flush_client_data_policy_logs
|
|
156
|
+
ensure
|
|
157
|
+
mutex.unlock if mutex&.owned?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Create and return the xolo-client-data package in Jamf Pro
|
|
161
|
+
#
|
|
162
|
+
# @return [Jamf::JPackage]
|
|
163
|
+
#####################
|
|
164
|
+
def create_client_data_jamf_package
|
|
165
|
+
progress "Jamf: Creating package object '#{CLIENT_DATA_PACKAGE_NAME}'"
|
|
166
|
+
|
|
167
|
+
info = "Installs the xolo client data JSON file into /Library/Application Support/xolo/#{CLIENT_DATA_FILE}"
|
|
168
|
+
|
|
169
|
+
# Create the package
|
|
170
|
+
pkg = Jamf::JPackage.create(
|
|
171
|
+
cnx: jamf_cnx,
|
|
172
|
+
packageName: CLIENT_DATA_PACKAGE_NAME,
|
|
173
|
+
fileName: CLIENT_DATA_PACKAGE_FILE,
|
|
174
|
+
categoryId: jamf_xolo_category_id,
|
|
175
|
+
info: info
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
pkg.save
|
|
179
|
+
# .pkg files are not uploaded here, but in the upload_client_data_package method
|
|
180
|
+
|
|
181
|
+
log_debug "Jamf: Created package '#{CLIENT_DATA_PACKAGE_NAME}'"
|
|
182
|
+
|
|
183
|
+
pkg
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
raise "Jamf: Error creating Jamf::JPackage '#{CLIENT_DATA_PACKAGE_NAME}': #{e.class}: #{e}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Create the xolo-client-data policies in Jamf Pro
|
|
189
|
+
#
|
|
190
|
+
# @return [void]
|
|
191
|
+
#####################
|
|
192
|
+
def create_client_data_policies_if_needed
|
|
193
|
+
all_pol_names = Jamf::Policy.all_names(cnx: jamf_cnx)
|
|
194
|
+
|
|
195
|
+
unless all_pol_names.include? CLIENT_DATA_AUTO_POLICY_NAME
|
|
196
|
+
create_client_data_policy CLIENT_DATA_AUTO_POLICY_NAME
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
return if all_pol_names.include? CLIENT_DATA_MANUAL_POLICY_NAME
|
|
200
|
+
|
|
201
|
+
create_client_data_policy CLIENT_DATA_MANUAL_POLICY_NAME
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Create a xolo-client-data install policy in Jamf Pro
|
|
205
|
+
#
|
|
206
|
+
# @param
|
|
207
|
+
#
|
|
208
|
+
# @return [void]
|
|
209
|
+
#####################
|
|
210
|
+
def create_client_data_policy(pol_name)
|
|
211
|
+
progress "Jamf: Creating policy '#{pol_name}'"
|
|
212
|
+
|
|
213
|
+
# Create the policy and set common attributes
|
|
214
|
+
pol = Jamf::Policy.create name: pol_name, cnx: jamf_cnx
|
|
215
|
+
pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
|
|
216
|
+
pol.add_package CLIENT_DATA_PACKAGE_NAME
|
|
217
|
+
|
|
218
|
+
# scope to all computers
|
|
219
|
+
pol.scope.set_all_targets
|
|
220
|
+
|
|
221
|
+
# exclude the forced exclusion group if any
|
|
222
|
+
if valid_forced_exclusion_group_name
|
|
223
|
+
pol.scope.set_exclusions :computer_groups, [valid_forced_exclusion_group_name]
|
|
224
|
+
log_info "Jamf: Excluded computer group: #{Xolo::Server.config.forced_exclusion} from policy '#{pol_name}'"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Set the trigger event and frequency
|
|
228
|
+
if pol_name == CLIENT_DATA_AUTO_POLICY_NAME
|
|
229
|
+
pol.set_trigger_event :checkin, true
|
|
230
|
+
pol.set_trigger_event :custom, Xolo::BLANK
|
|
231
|
+
pol.frequency = :daily
|
|
232
|
+
elsif pol_name == CLIENT_DATA_MANUAL_POLICY_NAME
|
|
233
|
+
pol.set_trigger_event :checkin, false
|
|
234
|
+
pol.set_trigger_event :custom, CLIENT_DATA_MANUAL_POLICY_TRIGGER
|
|
235
|
+
pol.frequency = :ongoing
|
|
236
|
+
else
|
|
237
|
+
err_msg = "Jamf: Invalid policy name '#{pol_name}' must be #{CLIENT_DATA_AUTO_POLICY_NAME} or #{CLIENT_DATA_MANUAL_POLICY_NAME}"
|
|
238
|
+
log_err err_msg, alert: true
|
|
239
|
+
return
|
|
240
|
+
end
|
|
241
|
+
pol.enable
|
|
242
|
+
|
|
243
|
+
pol.save
|
|
244
|
+
log_info "Jamf: Created policy '#{pol_name}'"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Flush the logs for the xolo-client-data policies
|
|
248
|
+
#
|
|
249
|
+
# @return [void]
|
|
250
|
+
#####################
|
|
251
|
+
def flush_client_data_policy_logs
|
|
252
|
+
progress "Jamf: Flushing logs for policy #{CLIENT_DATA_AUTO_POLICY_NAME}", log: :info
|
|
253
|
+
pol = Jamf::Policy.fetch name: CLIENT_DATA_AUTO_POLICY_NAME, cnx: jamf_cnx
|
|
254
|
+
pol.flush_logs
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Create the xolo-client-data package installer file.
|
|
258
|
+
# The xolo client executable is deployed as a separate thing in a Xolo Title
|
|
259
|
+
#
|
|
260
|
+
# @return [Pathname] the path to the new package file
|
|
261
|
+
#####################
|
|
262
|
+
def create_new_client_data_pkg_file
|
|
263
|
+
pkg_version = Time.now.strftime '%Y%m%d.%H%M%S.%6N'
|
|
264
|
+
work_dir_prefix = "#{CLIENT_DATA_PACKAGE_NAME}-#{pkg_version}"
|
|
265
|
+
|
|
266
|
+
pkg_work_dir = Pathname.new(Dir.mktmpdir(work_dir_prefix))
|
|
267
|
+
|
|
268
|
+
# The client data JSON file
|
|
269
|
+
root_dir = pkg_work_dir + 'pkgroot'
|
|
270
|
+
xolo_client_dir = root_dir + 'Library' + 'Application Support' + 'xolo'
|
|
271
|
+
xolo_client_dir.mkpath
|
|
272
|
+
client_data_file = xolo_client_dir + CLIENT_DATA_FILE
|
|
273
|
+
client_data_file.pix_save JSON.pretty_generate(client_data_hash)
|
|
274
|
+
|
|
275
|
+
# build the component package
|
|
276
|
+
progress "Jamf: Creating new client-data pkg file '#{CLIENT_DATA_PACKAGE_FILE}'", log: :info
|
|
277
|
+
|
|
278
|
+
unlock_signing_keychain
|
|
279
|
+
|
|
280
|
+
component_pkg_file = build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)
|
|
281
|
+
|
|
282
|
+
build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Build the component install pkg with pkgbuild
|
|
286
|
+
# NOTE: no need to shellescape the paths, since we are using the
|
|
287
|
+
# array version of Open3.capture2e
|
|
288
|
+
#
|
|
289
|
+
# @return [Pathname] the path to the new package file
|
|
290
|
+
#####################
|
|
291
|
+
def build_component_client_data_pkg_file(root_dir, pkg_version, pkg_work_dir)
|
|
292
|
+
outfile = pkg_work_dir + CLIENT_DATA_COMPONENT_PACKAGE_FILE
|
|
293
|
+
|
|
294
|
+
cmd = ['/usr/bin/pkgbuild']
|
|
295
|
+
cmd << '--root'
|
|
296
|
+
cmd << root_dir.to_s
|
|
297
|
+
cmd << '--identifier'
|
|
298
|
+
cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
|
|
299
|
+
cmd << '--version'
|
|
300
|
+
cmd << pkg_version
|
|
301
|
+
cmd << '--install-location'
|
|
302
|
+
cmd << '/'
|
|
303
|
+
cmd << '--sign'
|
|
304
|
+
cmd << Xolo::Server.config.pkg_signing_identity
|
|
305
|
+
cmd << '--keychain'
|
|
306
|
+
cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
|
|
307
|
+
cmd << outfile.to_s
|
|
308
|
+
|
|
309
|
+
log_debug "Command to build component pkg '#{CLIENT_DATA_COMPONENT_PACKAGE_FILE}': #{cmd.join(' ')}"
|
|
310
|
+
|
|
311
|
+
stdouterr, exit_status = Open3.capture2e(*cmd)
|
|
312
|
+
raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
|
|
313
|
+
|
|
314
|
+
outfile
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Build the distribution package for the xolo-client-data JSON file
|
|
318
|
+
# NOTE: no need to shellescape the paths, since we are using the
|
|
319
|
+
# array version of Open3.capture2e
|
|
320
|
+
#
|
|
321
|
+
# @return [Pathname] the path to the new package file
|
|
322
|
+
#####################
|
|
323
|
+
def build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
|
|
324
|
+
pkg_file = pkg_work_dir + CLIENT_DATA_PACKAGE_FILE
|
|
325
|
+
|
|
326
|
+
cmd = ['/usr/bin/productbuild']
|
|
327
|
+
cmd << '--package'
|
|
328
|
+
cmd << component_pkg_file.to_s
|
|
329
|
+
cmd << '--identifier'
|
|
330
|
+
cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
|
|
331
|
+
cmd << '--version'
|
|
332
|
+
cmd << pkg_version
|
|
333
|
+
cmd << '--sign'
|
|
334
|
+
cmd << Xolo::Server.config.pkg_signing_identity
|
|
335
|
+
cmd << '--keychain'
|
|
336
|
+
cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
|
|
337
|
+
cmd << pkg_file.to_s
|
|
338
|
+
|
|
339
|
+
log_debug "Command to build distribution pkg '#{CLIENT_DATA_PACKAGE_FILE}': #{cmd.join(' ')}"
|
|
340
|
+
|
|
341
|
+
stdouterr, exit_status = Open3.capture2e(*cmd)
|
|
342
|
+
raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
|
|
343
|
+
|
|
344
|
+
pkg_file
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# @return [Hash] the data to put in the xolo-client-data JSON file
|
|
348
|
+
#####################
|
|
349
|
+
def client_data_hash
|
|
350
|
+
cdh = {
|
|
351
|
+
titles: {}
|
|
352
|
+
}
|
|
353
|
+
all_title_objects.each do |title|
|
|
354
|
+
cdh[:titles][title.title] = title.to_h
|
|
355
|
+
cdh[:titles][title.title][:versions] = title.version_objects.map(&:to_h)
|
|
356
|
+
|
|
357
|
+
# the client uses the version_script to determine if a title is installed
|
|
358
|
+
cdh[:titles][title.title][:version_script] = title.version_script_contents if title.version_script
|
|
359
|
+
|
|
360
|
+
# add the forced_exclusion_group_name if any
|
|
361
|
+
if Xolo::Server.config.forced_exclusion
|
|
362
|
+
cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# add the frozen group name to the excluded_groups array
|
|
366
|
+
cdh[:titles][title.title][:excluded_groups] << title.jamf_frozen_group_name if title.jamf_frozen_group_name
|
|
367
|
+
end
|
|
368
|
+
# TESTING
|
|
369
|
+
# outfile = Pathname.new('/tmp/client-data.json')
|
|
370
|
+
# outfile.pix_save JSON.pretty_generate(cdh)
|
|
371
|
+
|
|
372
|
+
cdh
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# @return [Pathname] the path to the client executable 'xolo' in the ruby gem
|
|
376
|
+
#####################
|
|
377
|
+
def client_app_source
|
|
378
|
+
# parent 1 == helpers
|
|
379
|
+
# parent 2 == server
|
|
380
|
+
# parent 3 == xolo
|
|
381
|
+
# parent 4 == lib
|
|
382
|
+
# parent 5 == root
|
|
383
|
+
@client_app ||= Pathname.new(__FILE__).expand_path.parent.parent.parent.parent.parent + 'data' + 'client' + 'xolo'
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# temp
|
|
387
|
+
#####################
|
|
388
|
+
def client_data_testing
|
|
389
|
+
this_file = Pathname.new(__FILE__).expand_path
|
|
390
|
+
log_debug "this_file: #{this_file}"
|
|
391
|
+
# parent 1 == helpers
|
|
392
|
+
# parent 2 == server
|
|
393
|
+
# parent 3 == xolo
|
|
394
|
+
# parent 4 == lib
|
|
395
|
+
# parent 5 == root
|
|
396
|
+
data_dir = this_file.parent.parent.parent.parent.parent + 'data'
|
|
397
|
+
log_debug "data_dir: #{data_dir}"
|
|
398
|
+
log_debug "data_dir exists? #{data_dir.exist?}"
|
|
399
|
+
log_debug "data_dir children: #{data_dir.children}"
|
|
400
|
+
client_dir = data_dir + 'client'
|
|
401
|
+
log_debug "client_dir: #{client_dir}"
|
|
402
|
+
log_debug "client_dir exists? #{client_dir.exist?}"
|
|
403
|
+
log_debug "client_dir children: #{client_dir.children}"
|
|
404
|
+
client_app = client_dir + 'xolo'
|
|
405
|
+
log_debug "client_app: #{client_app}"
|
|
406
|
+
log_debug "client_app exists? #{client_app.exist?}"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
end # JamfPro
|
|
410
|
+
|
|
411
|
+
end # Helpers
|
|
412
|
+
|
|
413
|
+
end # Server
|
|
414
|
+
|
|
415
|
+
end # module Xolo
|
|
@@ -0,0 +1,265 @@
|
|
|
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 Helpers
|
|
16
|
+
|
|
17
|
+
module FileTransfers
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
#######################
|
|
21
|
+
#######################
|
|
22
|
+
|
|
23
|
+
# Module Methods
|
|
24
|
+
#######################
|
|
25
|
+
#######################
|
|
26
|
+
|
|
27
|
+
# when this module is included
|
|
28
|
+
def self.included(includer)
|
|
29
|
+
Xolo.verbose_include includer, self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Instance Methods
|
|
33
|
+
#######################
|
|
34
|
+
######################
|
|
35
|
+
|
|
36
|
+
# upload a file for testing ... anything
|
|
37
|
+
#################################
|
|
38
|
+
def process_incoming_testfile
|
|
39
|
+
progress 'starting test file upload', log: :debug
|
|
40
|
+
|
|
41
|
+
params[:file][:filename]
|
|
42
|
+
tempfile = Pathname.new params[:file][:tempfile].path
|
|
43
|
+
|
|
44
|
+
progress "1/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
|
|
45
|
+
sleep 2
|
|
46
|
+
progress "2/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
|
|
47
|
+
sleep 2
|
|
48
|
+
progress "3/3 TempFile is #{tempfile} size is #{tempfile.size}... is it still uploading?", log: :debug
|
|
49
|
+
progress 'all done', log: :debug
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Store an uploaded self service icon in the title's
|
|
53
|
+
# directory. It'll be added to Policies and Patch Policies as needed
|
|
54
|
+
# (increasing the bloat in the database, of course)
|
|
55
|
+
#################################
|
|
56
|
+
def process_incoming_ssvc_icon
|
|
57
|
+
filename = params[:file][:filename]
|
|
58
|
+
tempfile = Pathname.new params[:file][:tempfile].path
|
|
59
|
+
|
|
60
|
+
log_info "Processing uploaded SelfService icon for #{params[:title]}"
|
|
61
|
+
title = instantiate_title params[:title]
|
|
62
|
+
title.save_ssvc_icon(tempfile, filename)
|
|
63
|
+
title.configure_pol_for_self_service if title.self_service
|
|
64
|
+
rescue => e
|
|
65
|
+
msg = "#{e.class}: #{e}"
|
|
66
|
+
log_error msg
|
|
67
|
+
e.backtrace.each { |line| log_error "..#{line}" }
|
|
68
|
+
|
|
69
|
+
halt 400, { status: 400, error: msg }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Handle an uploaded pkg installer
|
|
73
|
+
# TODO: wrap this in a thread, it might be very slow for large pkgs.
|
|
74
|
+
# TODO: Also, when threaded, how to report errors?
|
|
75
|
+
# TODO: Split this into smaller methods
|
|
76
|
+
#############################
|
|
77
|
+
def process_incoming_pkg
|
|
78
|
+
log_info "Processing uploaded installer package for version '#{params[:version]}' of title '#{params[:title]}'"
|
|
79
|
+
|
|
80
|
+
# the Xolo::Server::Version that owns this pkg
|
|
81
|
+
version = instantiate_version title: params[:title], version: params[:version]
|
|
82
|
+
version.lock
|
|
83
|
+
|
|
84
|
+
# is this a re-upload? True if upload_date as any value in it
|
|
85
|
+
if version.upload_date.pix_empty?
|
|
86
|
+
action = 'Uploading'
|
|
87
|
+
re_uploading = false
|
|
88
|
+
else
|
|
89
|
+
re_uploading = true
|
|
90
|
+
action = 'Re-uploading'
|
|
91
|
+
version.log_change msg: 'Re-uploading pkg file'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# the original uploaded filename
|
|
95
|
+
orig_filename = params[:file][:filename]
|
|
96
|
+
log_debug "Incoming pkg file '#{orig_filename}' "
|
|
97
|
+
file_extname = validate_uploaded_pkg(orig_filename)
|
|
98
|
+
|
|
99
|
+
# Set the jamf_pkg_file, now that we know the extension
|
|
100
|
+
uploaded_pkg_name = "#{version.jamf_pkg_name}#{file_extname}"
|
|
101
|
+
log_debug "Jamf: Package filename will be '#{uploaded_pkg_name}'"
|
|
102
|
+
version.jamf_pkg_file = uploaded_pkg_name
|
|
103
|
+
|
|
104
|
+
# The tempfile created by Sinatra when the pkg was uploaded from xadm
|
|
105
|
+
tempfile = Pathname.new params[:file][:tempfile].path
|
|
106
|
+
|
|
107
|
+
# The uploaded tmpfile will be staged here before uploading again to
|
|
108
|
+
# the Jamf Dist Point(s)
|
|
109
|
+
staged_pkg = Xolo::Server::Title.title_dir(params[:title]) + uploaded_pkg_name
|
|
110
|
+
|
|
111
|
+
# remove any old one that might be there
|
|
112
|
+
staged_pkg.delete if staged_pkg.file?
|
|
113
|
+
|
|
114
|
+
if need_to_sign?(tempfile)
|
|
115
|
+
# This will put the signed pkg into the staged_pkg location
|
|
116
|
+
sign_uploaded_pkg(tempfile, staged_pkg)
|
|
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
|
|
124
|
+
|
|
125
|
+
# upload the pkg with the uploader tool defined in config
|
|
126
|
+
# This will set the checksum and manifest in the JPackage object
|
|
127
|
+
upload_to_dist_point(version.jamf_package, staged_pkg)
|
|
128
|
+
|
|
129
|
+
if re_uploading
|
|
130
|
+
# These must be set before calling wait_to_enable_reinstall_policy
|
|
131
|
+
version.reupload_date = Time.now
|
|
132
|
+
version.reuploaded_by = session[:admin]
|
|
133
|
+
|
|
134
|
+
# This will make the version start a thread
|
|
135
|
+
# that will wait some period of time (to allow for pkg uploads
|
|
136
|
+
# to complete) before enabling the reinstall policy
|
|
137
|
+
version.wait_to_enable_reinstall_policy
|
|
138
|
+
else
|
|
139
|
+
version.upload_date = Time.now
|
|
140
|
+
version.uploaded_by = session[:admin]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# make note if the pkg is a Distribution package
|
|
144
|
+
version.dist_pkg = pkg_is_distribution?(staged_pkg)
|
|
145
|
+
|
|
146
|
+
# save the manifest just in case
|
|
147
|
+
version.manifest_file.pix_atomic_write(version.jamf_package.manifest)
|
|
148
|
+
|
|
149
|
+
# save the checksum just in case
|
|
150
|
+
version.sha_512 = version.jamf_package.checksum
|
|
151
|
+
|
|
152
|
+
# don't save the admins local path to the pkg, just the filename they uploaded
|
|
153
|
+
version.pkg_to_upload = orig_filename
|
|
154
|
+
|
|
155
|
+
# save/update the local data file, since we've done stuff to update it
|
|
156
|
+
version.save_local_data
|
|
157
|
+
|
|
158
|
+
# log the upload
|
|
159
|
+
version.log_change msg: "#{action} pkg file '#{staged_pkg.basename}'"
|
|
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 }
|
|
169
|
+
ensure
|
|
170
|
+
version.unlock
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if a package is a Distribution package, if not,
|
|
174
|
+
# it is a component package and can't be used for
|
|
175
|
+
# MDM deployment.
|
|
176
|
+
#
|
|
177
|
+
# @param pkg_file [Pathname, String] The path to the .pkg
|
|
178
|
+
#
|
|
179
|
+
# @return [Boolean] true if the pkg is a Distribution package
|
|
180
|
+
###########################################
|
|
181
|
+
def pkg_is_distribution?(pkg_file)
|
|
182
|
+
pkg_file = Pathname.new(pkg_file)
|
|
183
|
+
raise ArgumentError, "pkg_file does not exist or not a file: #{pkg_file}" unless pkg_file.file?
|
|
184
|
+
|
|
185
|
+
tmpdir = Pathname.new(Dir.mktmpdir)
|
|
186
|
+
workdir = tmpdir + "#{pkg_file.basename}-expanded"
|
|
187
|
+
|
|
188
|
+
system "/usr/sbin/pkgutil --expand #{pkg_file.to_s.shellescape} #{workdir.to_s.shellescape}"
|
|
189
|
+
|
|
190
|
+
workdir.children.map(&:basename).map(&:to_s).include? 'Distribution'
|
|
191
|
+
ensure
|
|
192
|
+
tmpdir.rmtree
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# upload a staged pkg to the dist point(s)
|
|
196
|
+
# This will also update the checksum and manifest.
|
|
197
|
+
#
|
|
198
|
+
# @param jpkg [Jamf::JPackage] The package object for which the pkg is being uploaded
|
|
199
|
+
# @param pkg_file [Pathname] The path to .pkg file being uploaded
|
|
200
|
+
#
|
|
201
|
+
# @return [void]
|
|
202
|
+
###########################################
|
|
203
|
+
def upload_to_dist_point(jpkg, pkg_file)
|
|
204
|
+
if Xolo::Server.config.upload_tool.to_s.downcase == 'api'
|
|
205
|
+
jpkg.upload pkg_file # this will update the checksum and manifest automatically, and save back to the server
|
|
206
|
+
log_info "Jamf: Uploaded #{pkg_file.basename} to primary dist point via API, with new checksum and manifest"
|
|
207
|
+
else
|
|
208
|
+
log_debug "Jamf: Regenerating manifest for package '#{jpkg.packageName}' from #{pkg_file.basename}"
|
|
209
|
+
jpkg.generate_manifest(pkg_file)
|
|
210
|
+
|
|
211
|
+
log_debug "Jamf: Recalculating checksum for package '#{jpkg.packageName}' from #{pkg_file.basename}"
|
|
212
|
+
jpkg.recalculate_checksum(pkg_file)
|
|
213
|
+
|
|
214
|
+
log_info "Jamf: Saving package '#{jpkg.packageName}' with new checksum and manifest"
|
|
215
|
+
jpkg.save
|
|
216
|
+
upload_via_tool(jpkg, pkg_file)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# upload the pkg with the uploader tool defined in config
|
|
221
|
+
#
|
|
222
|
+
# @param version [Xolo::Server::Version] The version object
|
|
223
|
+
# @param staged_pkg [Pathname] The path to the staged pkg
|
|
224
|
+
#
|
|
225
|
+
# @return [void]
|
|
226
|
+
###########################################
|
|
227
|
+
def upload_via_tool(jpkg, pkg_file)
|
|
228
|
+
log_info "Jamf: Uploading #{pkg_file.basename} to dist point(s) via upload tool"
|
|
229
|
+
|
|
230
|
+
tool = Shellwords.escape Xolo::Server.config.upload_tool.to_s
|
|
231
|
+
jpkg_name = Shellwords.escape jpkg.packageName
|
|
232
|
+
pkg = Shellwords.escape pkg_file.to_s
|
|
233
|
+
cmd = "#{tool} #{jpkg_name} #{pkg}"
|
|
234
|
+
|
|
235
|
+
stdouterr, exit_status = Open3.capture2e(cmd)
|
|
236
|
+
return if exit_status.success?
|
|
237
|
+
|
|
238
|
+
msg = "Uploader tool failed to upload #{pkg_file.basename} to dist point(s): #{stdouterr}"
|
|
239
|
+
log_error msg
|
|
240
|
+
raise msg
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Confirm and return the extension of the originally uplaoded file,
|
|
244
|
+
# either .pkg or .zip
|
|
245
|
+
#
|
|
246
|
+
# @param filename [String] The original name of the file uploaded to Xolo.
|
|
247
|
+
#
|
|
248
|
+
# @return [String] either '.pkg' or '.zip'
|
|
249
|
+
###############################
|
|
250
|
+
def validate_uploaded_pkg(filename)
|
|
251
|
+
log_debug "Validating pkg file ext for '#{filename}'"
|
|
252
|
+
|
|
253
|
+
file_extname = Pathname.new(filename).extname
|
|
254
|
+
return file_extname if Xolo::OK_PKG_EXTS.include? file_extname
|
|
255
|
+
|
|
256
|
+
raise "Bad filename '#{filename}'. Package files must end in .pkg or .zip (for old-style bundle packages)"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
end # FileTransfers
|
|
260
|
+
|
|
261
|
+
end # Helpers
|
|
262
|
+
|
|
263
|
+
end # Server
|
|
264
|
+
|
|
265
|
+
end # module Xolo
|