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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +177 -0
  3. data/README.md +7 -0
  4. data/bin/xoloserver +106 -0
  5. data/data/com.pixar.xoloserver.plist +29 -0
  6. data/data/uninstall-pkgs-by-id.zsh +103 -0
  7. data/lib/xolo/server/app.rb +133 -0
  8. data/lib/xolo/server/command_line.rb +216 -0
  9. data/lib/xolo/server/configuration.rb +739 -0
  10. data/lib/xolo/server/constants.rb +70 -0
  11. data/lib/xolo/server/helpers/auth.rb +257 -0
  12. data/lib/xolo/server/helpers/client_data.rb +415 -0
  13. data/lib/xolo/server/helpers/file_transfers.rb +265 -0
  14. data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
  15. data/lib/xolo/server/helpers/log.rb +97 -0
  16. data/lib/xolo/server/helpers/maintenance.rb +401 -0
  17. data/lib/xolo/server/helpers/notification.rb +145 -0
  18. data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
  19. data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
  20. data/lib/xolo/server/helpers/title_editor.rb +92 -0
  21. data/lib/xolo/server/helpers/titles.rb +145 -0
  22. data/lib/xolo/server/helpers/versions.rb +160 -0
  23. data/lib/xolo/server/log.rb +286 -0
  24. data/lib/xolo/server/mixins/changelog.rb +315 -0
  25. data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
  26. data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
  27. data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
  28. data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
  29. data/lib/xolo/server/object_locks.rb +102 -0
  30. data/lib/xolo/server/routes/auth.rb +89 -0
  31. data/lib/xolo/server/routes/jamf_pro.rb +89 -0
  32. data/lib/xolo/server/routes/maint.rb +174 -0
  33. data/lib/xolo/server/routes/title_editor.rb +71 -0
  34. data/lib/xolo/server/routes/titles.rb +285 -0
  35. data/lib/xolo/server/routes/uploads.rb +93 -0
  36. data/lib/xolo/server/routes/versions.rb +261 -0
  37. data/lib/xolo/server/routes.rb +168 -0
  38. data/lib/xolo/server/title.rb +1143 -0
  39. data/lib/xolo/server/version.rb +902 -0
  40. data/lib/xolo/server.rb +205 -0
  41. data/lib/xolo-server.rb +8 -0
  42. metadata +243 -0
@@ -0,0 +1,261 @@
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
+ # frozen_string_literal: true
8
+
9
+ # main module
10
+ module Xolo
11
+
12
+ # Server Module
13
+ module Server
14
+
15
+ module Routes
16
+
17
+ module Versions
18
+
19
+ # This is how we 'mix in' modules to Sinatra servers
20
+ # for route definitions and similar things
21
+ #
22
+ # (things to be 'included' for use in route and view processing
23
+ # are mixed in by delcaring them to be helpers)
24
+ #
25
+ # We make them extentions here with
26
+ # extend Sinatra::Extension (from sinatra-contrib)
27
+ # and then 'register' them in the server with
28
+ # register Xolo::Server::<Module>
29
+ # Doing it this way allows us to split the code into a logical
30
+ # file structure, without re-opening the Sinatra::Base server app,
31
+ # and let xeitwork do the requiring of those files
32
+ extend Sinatra::Extension
33
+
34
+ # when this module is included
35
+ def self.included(includer)
36
+ Xolo.verbose_include includer, self
37
+ end
38
+
39
+ # @return [String] The default min_os for versions
40
+ #################################
41
+ get '/default_min_os' do
42
+ resp = { min_os: default_min_os.to_s }
43
+ body resp
44
+ end
45
+
46
+ # Create a new version from the body content of the request
47
+ #
48
+ # @return [Hash] A response hash
49
+ #################################
50
+ post '/titles/:title/versions' do
51
+ request.body.rewind
52
+ data = parse_json request.body.read
53
+
54
+ unless data[:title] == params[:title]
55
+ halt 400,
56
+ "Path/Data Mismatch! params[:title] => '#{params[:title]}' / data[:title] => '#{data[:title]}'"
57
+ end
58
+ data[:min_os] = default_min_os if data[:min_os].pix_empty?
59
+
60
+ log_debug "Incoming new version data: #{data}"
61
+ log_debug "Incoming new version data: #{data.class}"
62
+
63
+ vers = instantiate_version(data)
64
+ halt_on_existing_version vers.title, vers.version
65
+
66
+ if vers.title_object.jamf_patch_ea_awaiting_acceptance? && !Xolo::Server.config.jamf_auto_accept_xolo_eas
67
+
68
+ log_info "Jamf: Patch Title '#{params[:title]}' version_script must be manually accepted as a Patch EA before version can be activated. Admin has been notified."
69
+
70
+ raise Xolo::ActionRequiredError,
71
+ "This title has a version-script, which must be accepted manually in Jamf Pro at #{vers.title_object.jamf_patch_ea_url} under the 'Extension Attribute' tab (click 'Edit'). Please do that and try again"
72
+ end
73
+
74
+ log_info "Admin #{session[:admin]} is creating version #{data[:version]} of title '#{params[:title]}'"
75
+
76
+ Xolo::Server.rw_lock(data[:title], data[:version]).with_write_lock do
77
+ with_streaming do
78
+ vers.create
79
+ update_client_data
80
+ end
81
+ end
82
+ end
83
+
84
+ # get a list of versions for a title
85
+ # @return [Array<Hash>] the names of existing versions for the title
86
+ #################################
87
+ get '/titles/:title/versions' do
88
+ halt_on_missing_title params[:title]
89
+
90
+ log_debug "Admin #{session[:admin]} is listing all versions for title '#{params[:title]}'"
91
+ # body all_versions(params[:title])
92
+ vers_ins = all_version_instances(params[:title])
93
+ # log_debug "vers_ins: #{vers_ins}"
94
+ body vers_ins.map(&:to_h)
95
+ end
96
+
97
+ # get all the data for a single version
98
+ # @return [Hash] The data for this version
99
+ #################################
100
+ get '/titles/:title/versions/:version' do
101
+ Xolo::Server.rw_lock(params[:title], params[:version]).with_read_lock do
102
+ log_debug "Admin #{session[:admin]} is fetching version '#{params[:version]}' of title '#{params[:title]}'"
103
+ halt_on_missing_version params[:title], params[:version]
104
+
105
+ vers = instantiate_version title: params[:title], version: params[:version]
106
+ body vers.to_h
107
+ end
108
+ end
109
+
110
+ # Update a version,
111
+ # Replace the data for an existing version with the content of the request
112
+ # @return [Hash] A response hash
113
+ #################################
114
+ put '/titles/:title/versions/:version' do
115
+ request.body.rewind
116
+ new_data = parse_json(request.body.read)
117
+ new_data[:min_os] = default_min_os if new_data[:min_os].pix_empty?
118
+ log_debug "Incoming update version data: #{new_data}"
119
+
120
+ halt_on_missing_title params[:title]
121
+ halt_on_missing_version params[:title], params[:version]
122
+ halt_on_locked_version params[:title], params[:version]
123
+
124
+ vers = instantiate_version title: params[:title], version: params[:version]
125
+
126
+ with_streaming do
127
+ vers.update new_data
128
+ update_client_data
129
+ end
130
+ end
131
+
132
+ # Delete an existing version
133
+ #
134
+ # This route sends a streamed response indicating progress
135
+ # in realtime, not a JSON object.
136
+ #
137
+ # @return [Hash] A response hash
138
+ #################################
139
+ delete '/titles/:title/versions/:version' do
140
+ halt_on_missing_version params[:title], params[:version]
141
+ halt_on_locked_version params[:title], params[:version]
142
+
143
+ log_info "Admin #{session[:admin]} is deleting version '#{params[:version]}' of title '#{params[:title]}'"
144
+
145
+ # for some reason, instantiating in the with_streaming block
146
+ # causes a throw error
147
+ vers = instantiate_version title: params[:title], version: params[:version]
148
+
149
+ with_streaming do
150
+ vers.delete
151
+ update_client_data
152
+ end
153
+ end
154
+
155
+ # upload a pkg for a version
156
+ # param with the uploaded file must be :file
157
+ ######################
158
+ post '/titles/:title/versions/:version/pkg' do
159
+ process_incoming_pkg
160
+ body({ result: :uploaded })
161
+ end
162
+
163
+ # Install a version on computers and/or a group, via
164
+ # the Jamf API's deploy_package endpoint and the
165
+ # InstallEnterpriseApplication MDM command.
166
+ #
167
+ # Request body is a JSON object with the following keys
168
+ # computers: [Array<String, Integer>] The computer identifiers to install on.
169
+ # Identifiers are either serial numbers, names, or Jamf IDs.
170
+ # groups: [Array<String, Integer>] Identifiers of the groups to install on.
171
+ #
172
+ # Response body is a JSON object with the following keys
173
+ # removals: [Array<Hash>] { device: <Integer>, group: <Integer>, reason: <String> }
174
+ # queuedCommands: [Array<Hash>] { device: <Integer>, commandUuid: <String> }
175
+ # errors: [Array<Hash>] { device: <Integer>, group: <Integer>, reason: <String> }
176
+ ######################
177
+ post '/titles/:title/versions/:version/deploy' do
178
+ request.body.rewind
179
+ targets = parse_json(request.body.read)
180
+
181
+ log_info "Incoming MDM deployment from admin #{session[:admin]} for title '#{params[:title]}', version '#{params[:version]}'."
182
+ log_info "MDM deployment targets: #{targets}"
183
+
184
+ halt_on_missing_version params[:title], params[:version]
185
+
186
+ vers = instantiate_version title: params[:title], version: params[:version]
187
+
188
+ result = vers.deploy_via_mdm targets
189
+
190
+ body result
191
+ rescue => e
192
+ msg = "#{e.class}: #{e}"
193
+ log_error msg
194
+ halt 400, { status: 400, error: msg }
195
+ end
196
+
197
+ # run 'repair' on a version
198
+ ######################
199
+ post '/titles/:title/versions/:version/repair' do
200
+ log_debug "Admin #{session[:admin]} is repairing version '#{params[:version]}' of title '#{params[:title]}'"
201
+ halt_on_missing_version params[:title], params[:version]
202
+ halt_on_locked_version params[:title], params[:version]
203
+
204
+ vers = instantiate_version title: params[:title], version: params[:version]
205
+
206
+ with_streaming do
207
+ vers.repair
208
+ update_client_data
209
+ end
210
+ end
211
+
212
+ # Return info about all the computers with a given version of a title installed
213
+ #
214
+ # @return [Array<Hash>] The data for all computers with the given version of the title
215
+ #################################
216
+ get '/titles/:title/versions/:version/patch_report' do
217
+ log_debug "Admin #{session[:admin]} is fetching patch report for version #{params[:version]} title '#{params[:title]}'"
218
+
219
+ if params[:version] == Xolo::UNKNOWN
220
+ halt_on_missing_title params[:title]
221
+ title = instantiate_title params[:title]
222
+ data = title.patch_report vers: Xolo::UNKNOWN
223
+
224
+ else
225
+ halt_on_missing_version params[:title], params[:version]
226
+ version = instantiate_version title: params[:title], version: params[:version]
227
+ data = version.patch_report
228
+ end
229
+
230
+ body data
231
+ end
232
+
233
+ # Return URLs for all the UI pages for a version
234
+ #
235
+ # @return [Hash] The URLs for all the UI pages for a version
236
+ #################################
237
+ get '/titles/:title/versions/:version/urls' do
238
+ log_debug "Admin #{session[:admin]} is fetching GUI URLS for version #{params[:version]} of title '#{params[:title]}'"
239
+
240
+ halt_on_missing_version params[:title], params[:version]
241
+ vers = instantiate_version title: params[:title], version: params[:version]
242
+ data = {
243
+ ted_patch_url: vers.ted_patch_url,
244
+ jamf_auto_install_policy_url: vers.jamf_auto_install_policy_url,
245
+ jamf_manual_install_policy_url: vers.jamf_manual_install_policy_url,
246
+ jamf_patch_policy_url: vers.jamf_patch_policy_url,
247
+ jamf_package_url: vers.jamf_package_url
248
+ }
249
+ data[:jamf_version_installed_group] = vers.jamf_installed_group_url if vers.jamf_installed_group_exist?
250
+ data[:jamf_auto_reinstall_policy_url] = vers.jamf_auto_reinstall_policy_url if vers.jamf_auto_reinstall_policy_exist?
251
+
252
+ body data
253
+ end
254
+
255
+ end # Versions
256
+
257
+ end # Routes
258
+
259
+ end # Server
260
+
261
+ end # module Xolo
@@ -0,0 +1,168 @@
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
+ # frozen_string_literal: true
8
+
9
+ # main module
10
+ module Xolo
11
+
12
+ # Server Module
13
+ module Server
14
+
15
+ # Some "global" routes are defined here.
16
+ # most are defined in other modules.
17
+ ##################################
18
+ module Routes
19
+
20
+ # This is how we extend modules to Sinatra servers:
21
+ # We make them extentions here with
22
+ # extend Sinatra::Extension (from sinatra-contrib)
23
+ # and then 'register' them in the server with
24
+ # register Xolo::Server::<Module>
25
+ # Doing it this way allows us to split the code into a logical
26
+ # file structure, without re-opening the Sinatra::Base server app,
27
+ # and let xeitwork do the requiring of those files.
28
+ #
29
+ # To 'include' modules in Sinatra servers, you declare them as helpers.
30
+ #
31
+ extend Sinatra::Extension
32
+
33
+ # pre-process
34
+ ##############
35
+ before do
36
+ halt 503, { status: 503, error: 'Server is shutting down' } if Xolo::Server.shutting_down? && !request.path.start_with?('/streamed_progress/')
37
+
38
+ adm = session[:admin] ? ", admin '#{session[:admin]}'" : Xolo::BLANK
39
+ log_info "Processing #{request.request_method} #{request.path} from #{request.ip}#{adm}"
40
+
41
+ log_debug "Session in before-filter: #{session.inspect}"
42
+
43
+ # these routes don't need an auth'd session
44
+ log_debug "Checking if request path '#{request.path}' is in NO_AUTH_ROUTES or NO_AUTH_PREFIXES"
45
+ break if Xolo::Server::Helpers::Auth::NO_AUTH_ROUTES.include? request.path
46
+ break if Xolo::Server::Helpers::Auth::NO_AUTH_PREFIXES.any? { |pfx| request.path.start_with? pfx }
47
+
48
+ # these routes are expected to be called by the xolo server itself
49
+ log_debug "Checking if request path '#{request.path}' is in INTERNAL_ROUTES"
50
+ break if Xolo::Server::Helpers::Auth::INTERNAL_ROUTES.include?(request.path) && valid_internal_auth_token?
51
+
52
+ # these routes are for server admins only, and require an authenticated session
53
+ log_debug "Checking if request path '#{request.path}' is in SERVER_ADMIN_ROUTES"
54
+ break if Xolo::Server::Helpers::Auth::SERVER_ADMIN_ROUTES.include?(request.path) && valid_server_admin?
55
+
56
+ # If here, we must have a session cookie marked as 'authenticated'
57
+ halt 401, { status: 401, error: 'You must log in to the Xolo server' } unless session[:authenticated]
58
+ end
59
+
60
+ # error process
61
+ ##############
62
+ error do
63
+ log_debug 'Running error filter'
64
+
65
+ resp_body = { status: response.status, error: env['sinatra.error'].message }
66
+ body resp_body
67
+ end
68
+
69
+ # post-process
70
+ # Convert the body to JSON unless @no_json is set
71
+ ##############
72
+ after do
73
+ # log a stack trace if the response status is 4xx or 5xx, or if there's a sinatra.error
74
+ if response.status >= 400
75
+ log_error "Response status #{response.status} for #{request.request_method} #{request.path}"
76
+ if env['sinatra.error']
77
+ log_error "Sinatra Error message: #{env['sinatra.error'].message}"
78
+ env['sinatra.error'].backtrace.each { |line| log_error "..#{line}" }
79
+ else
80
+ log_error 'Xolo Stack trace:'
81
+ caller.each { |line| log_error "..#{line}" }
82
+ end
83
+ end
84
+
85
+ if @no_json
86
+ log_debug 'NOT converting body to JSON in after filter'
87
+ else
88
+ log_debug 'Converting body to JSON in after filter'
89
+ content_type :json
90
+ # IMPORTANT, this only works if you remember to explicitly use
91
+ # `body body_content` in every route.
92
+ # You can't just define the body
93
+ # by the last evaluated statement of the route.
94
+ #
95
+ response.body = JSON.dump(response.body)
96
+ end
97
+
98
+ # TODO: See if there's any appropate place to disconnect
99
+ # from ruby-jss and windoo api connections?
100
+ # perhaps a callback to when a Sinatra server instance
101
+ # 'finishes'?
102
+ # can't
103
+ end
104
+
105
+ # Ping
106
+ ##########
107
+ get '/ping' do
108
+ @no_json = true
109
+ body 'pong'
110
+ end
111
+
112
+ # The streamed progress updates
113
+ # The stream_file param should be in the URL query, i.e.
114
+ # "/streamed_progress/?stream_file=<url-escaped path to file>"
115
+ #
116
+ ################
117
+ get '/streamed_progress/' do
118
+ log_debug "Starting progress stream from file: #{params[:stream_file]}"
119
+ # make note that this Server instance is just streaming from a file
120
+ # not acuatlly processing anything.
121
+ @streaming_from_file = true
122
+
123
+ @no_json = true
124
+ stream_file = Pathname.new params[:stream_file]
125
+
126
+ stream do |stream_out|
127
+ stream_progress(stream_file: stream_file, stream: stream_out)
128
+ rescue => e
129
+ stream_out << "ERROR DURING PROGRESS STREAM: #{e.class}: #{e}"
130
+ ensure
131
+ stream_out.close
132
+ end
133
+ end
134
+
135
+ # test
136
+ ##########
137
+ get '/test' do
138
+ # Xolo::Server::Helpers::Maintenance.post_to_start_cleanup force: true
139
+ # result = { result: 'posted to start cleanup' }
140
+
141
+ # send_email(
142
+ # to: 'xolo@pixar.com',
143
+ # subject: 'Test Email from Xolo Server',
144
+ # msg: 'This is a test email from the Xolo Server'
145
+ # )
146
+ # result = { result: 'message sent' }
147
+
148
+ # client_data_testing
149
+ # update_client_data
150
+
151
+ result = { result: 'test' }
152
+
153
+ body result
154
+ end
155
+
156
+ end # Routes
157
+
158
+ end # Server
159
+
160
+ end # module Xolo
161
+
162
+ require 'xolo/server/routes/auth'
163
+ require 'xolo/server/routes/jamf_pro'
164
+ require 'xolo/server/routes/maint'
165
+ require 'xolo/server/routes/title_editor'
166
+ require 'xolo/server/routes/titles'
167
+ require 'xolo/server/routes/uploads'
168
+ require 'xolo/server/routes/versions'