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,174 @@
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 Maint
18
+
19
+ # This is how we 'mix in' modules to Sinatra servers:
20
+ # We make them extentions here with
21
+ # extend Sinatra::Extension (from sinatra-contrib)
22
+ # and then 'register' them in the server with
23
+ # register Xolo::Server::<Module>
24
+ # Doing it this way allows us to split the code into a logical
25
+ # file structure, without re-opening the Sinatra::Base server app,
26
+ extend Sinatra::Extension
27
+
28
+ # when this module is included
29
+ def self.included(includer)
30
+ Xolo.verbose_include includer, self
31
+ end
32
+
33
+ # when this module is extended
34
+ def self.extended(extender)
35
+ Xolo.verbose_extend extender, self
36
+ end
37
+
38
+ # Threads
39
+ ##########
40
+ get '/maint/threads' do
41
+ body Xolo::Server.thread_info
42
+ end
43
+
44
+ # State
45
+ ##########
46
+ get '/maint/state' do
47
+ require 'concurrent/version'
48
+ uptime_secs = (Time.now - Xolo::Server.start_time).to_i
49
+
50
+ state = {
51
+ start_time: Xolo::Server.start_time,
52
+ uptime: uptime_secs.pix_humanize_secs,
53
+ uptime_secs: uptime_secs,
54
+ app_env: Xolo::Server.app_env,
55
+ data_dir: Xolo::Server::DATA_DIR,
56
+ log_file: Xolo::Server::Log::LOG_FILE,
57
+ log_level: Xolo::Server::Log::LEVELS[Xolo::Server.logger.level],
58
+ ruby_version: RUBY_VERSION,
59
+ gems: {
60
+ xolo_version: Xolo::VERSION,
61
+ ruby_jss_version: Jamf::VERSION,
62
+ windoo_version: Windoo::VERSION,
63
+ sinatra_version: Sinatra::VERSION,
64
+ thin_version: Thin::VERSION::STRING,
65
+ concurrent_ruby_version: Concurrent::VERSION,
66
+ faraday_version: Faraday::VERSION
67
+ },
68
+ config: Xolo::Server.config.to_h_private
69
+ }
70
+
71
+ if params[:extended]
72
+ state[:gem_path] = Gem.paths.path
73
+ state[:load_path] = $LOAD_PATH
74
+ state[:object_locks] = Xolo::Server.object_locks
75
+ state[:pkg_deletion_pool] = Xolo::Server::Version.pkg_deletion_pool_info
76
+ state[:threads] = Xolo::Server.thread_info
77
+ end
78
+
79
+ body state
80
+ end
81
+
82
+ # run the cleanup process from the internal timer task
83
+ # The before filter will ensure the request came from the server itself.
84
+ # with a valid internal auth token.
85
+ ################
86
+ post '/maint/cleanup-internal' do
87
+ log_info 'Starting internal cleanup'
88
+ session[:admin] = 'Automated Cleanup'
89
+
90
+ thr = Thread.new { run_cleanup }
91
+ thr.name = 'Internal Cleanup Thread'
92
+ result = { result: 'Internal Cleanup Underway' }
93
+ body result
94
+ end
95
+
96
+ # run the cleanup process manually from a server admin via xadm
97
+ ################
98
+ post '/maint/cleanup' do
99
+ log_info "Starting manual server cleanup by #{session[:admin]}"
100
+
101
+ session[:admin] = "Cleanup by #{session[:admin]}"
102
+ thr = Thread.new { run_cleanup }
103
+ thr.name = 'Manual Cleanup Thread'
104
+ result = { result: 'Manual Cleanup Underway' }
105
+ body result
106
+ end
107
+
108
+ # force an update of the client data
109
+ ################
110
+ post '/maint/update-client-data' do
111
+ log_info "Force update of client-data by #{session[:admin]}"
112
+ log_debug "Gem Paths: #{Gem.paths.inspect}"
113
+
114
+ thr = Thread.new { update_client_data }
115
+ thr.name = 'Manual Client Data Update Thread'
116
+ result = { result: 'Client Data Update underway' }
117
+ body result
118
+ end
119
+
120
+ # force log rotation
121
+ ################
122
+ post '/maint/rotate-logs' do
123
+ log_info "Force log rotation by #{session[:admin]}"
124
+
125
+ thr = Thread.new { Xolo::Server::Log.rotate_logs force: true }
126
+ thr.name = 'Manual Log Rotation Thread'
127
+ result = { result: 'Log rotation underway' }
128
+ body result
129
+ end
130
+
131
+ # set the log level
132
+ ################
133
+ post '/maint/set-log-level' do
134
+ request.body.rewind
135
+ payload = parse_json request.body.read
136
+ level = payload[:level]
137
+
138
+ log_info "Setting log level to #{level} by #{session[:admin]}"
139
+ Xolo::Server.set_log_level level, admin: session[:admin]
140
+
141
+ result = { result: "Log level set to #{level}" }
142
+ body result
143
+ end
144
+
145
+ # Shutdown the server gracefully
146
+ # stop accepting new requests
147
+ # wait for all queues and threads to finish, including:
148
+ # the cleanup timer task & mutex
149
+ # the log rotation timer task & mutex
150
+ # the pkg deletion pool
151
+ # the object locks
152
+ # the progress streams, including this one, which will be the last thing to finish
153
+ ################
154
+ post '/maint/shutdown-server' do
155
+ request.body.rewind
156
+ payload = parse_json request.body.read
157
+ restart = payload[:restart]
158
+
159
+ # for streamed responses, the with_streaming block must
160
+ # be the last thing in the route
161
+ with_streaming do
162
+ # give the stream a chance to get started
163
+ sleep 2
164
+ shutdown_server restart
165
+ end
166
+ end
167
+
168
+ end # module maint
169
+
170
+ end # Routes
171
+
172
+ end # Server
173
+
174
+ end # module Xolo
@@ -0,0 +1,71 @@
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
+ # See comments for Xolo::Server::Helpers::TitleEditor
18
+ #
19
+ module TitleEditor
20
+
21
+ # This is how we 'mix in' modules to Sinatra servers
22
+ # for route definitions and similar things
23
+ #
24
+ # (things to be 'included' for use in route and view processing
25
+ # are mixed in by delcaring them to be helpers)
26
+ #
27
+ # We make them extentions here with
28
+ # extend Sinatra::Extension (from sinatra-contrib)
29
+ # and then 'register' them in the server with
30
+ # register Xolo::Server::<Module>
31
+ # Doing it this way allows us to split the code into a logical
32
+ # file structure, without re-opening the Sinatra::Base server app,
33
+ # and let xeitwork do the requiring of those files
34
+ extend Sinatra::Extension
35
+
36
+ # Module methods
37
+ #
38
+ ##############################
39
+ ##############################
40
+
41
+ # when this module is included
42
+ def self.included(includer)
43
+ Xolo.verbose_include includer, self
44
+ end
45
+
46
+ # when this module is extended
47
+ def self.extended(extender)
48
+ Xolo.verbose_extend extender, self
49
+ end
50
+
51
+ # Routes
52
+ #
53
+ ##############################
54
+ ##############################
55
+
56
+ ###############
57
+ get '/title-editor/titles' do
58
+ log_debug "Fetching Title Editor titles for #{session[:admin]}"
59
+ wcnx = ted_cnx
60
+ body Windoo::SoftwareTitle.all(cnx: wcnx).map { |t| t[:id] }.sort
61
+ ensure
62
+ wcnx&.disconnect
63
+ end
64
+
65
+ end # Module
66
+
67
+ end # Routes
68
+
69
+ end # Server
70
+
71
+ end # module Xolo
@@ -0,0 +1,285 @@
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 Titles
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
+ extend Sinatra::Extension
32
+
33
+ # when this module is included
34
+ def self.included(includer)
35
+ Xolo.verbose_include includer, self
36
+ end
37
+
38
+ # Create a new title from the body content of the request
39
+ #
40
+ # @return [Hash] A response hash
41
+ #################################
42
+ post '/titles' do
43
+ request.body.rewind
44
+ data = parse_json(request.body.read)
45
+ log_debug "Incoming new title data: #{data}"
46
+
47
+ title = instantiate_title data
48
+ halt_on_existing_title title.title
49
+
50
+ log_info "Admin #{session[:admin]} is creating title '#{title.title}'"
51
+ with_streaming do
52
+ title.create
53
+ # we don't need to update client data when titles are created
54
+ # because they don't have any versions yet, so there's nothing a
55
+ # client can do with them.
56
+ end
57
+ end
58
+
59
+ # get an array of titles
60
+ # @return [Array<Hash>] the data for existing titles
61
+ #################################
62
+ get '/titles' do
63
+ log_debug "Admin #{session[:admin]} is fetching all titles"
64
+ body all_title_objects.map(&:to_h)
65
+ end
66
+
67
+ # get all the data for a single title
68
+ # @return [Hash] The data for this title
69
+ #################################
70
+ get '/titles/:title' do
71
+ log_debug "Admin #{session[:admin]} is fetching title '#{params[:title]}'"
72
+ halt_on_missing_title params[:title]
73
+
74
+ title = instantiate_title params[:title]
75
+
76
+ body title.to_h
77
+ end
78
+
79
+ # Update a title by
80
+ # replacing the data for an existing title with the content of the request
81
+ # @return [Hash] A response hash
82
+ #################################
83
+ put '/titles/:title' do
84
+ halt_on_missing_title params[:title]
85
+ halt_on_locked_title params[:title]
86
+
87
+ title = instantiate_title params[:title]
88
+
89
+ request.body.rewind
90
+ new_data = parse_json(request.body.read)
91
+ log_debug "Incoming update title data: #{new_data}"
92
+
93
+ log_info "Admin #{session[:admin]} is updating title '#{params[:title]}'"
94
+ with_streaming do
95
+ title.update new_data
96
+ update_client_data
97
+ end
98
+ end
99
+
100
+ # Release a version of a title
101
+ #
102
+ # @return [Hash] A response hash
103
+ #################################
104
+ patch '/titles/:title/release/:version' do
105
+ log_info "Admin #{session[:admin]} is releasing version #{params[:version]} of title '#{params[:title]}' via PATCH"
106
+
107
+ halt_on_missing_title params[:title]
108
+ halt_on_missing_version params[:title], params[:version]
109
+ halt_on_locked_title params[:title]
110
+ halt_on_locked_version params[:title], params[:version]
111
+
112
+ title = instantiate_title params[:title]
113
+
114
+ if title.released_version == params[:version]
115
+ msg = "Version '#{params[:version]}' of title '#{params[:title]}' is already released"
116
+ log_debug "ERROR: #{msg}"
117
+ halt 409, { status: 409, error: msg }
118
+ end
119
+
120
+ vers_to_release = params[:version]
121
+
122
+ with_streaming do
123
+ title.release vers_to_release
124
+ update_client_data
125
+ end
126
+ end
127
+
128
+ # run 'repair' on a title
129
+ # to do all versions, body should be JSON { "repair_versions": true }
130
+ ######################
131
+ post '/titles/:title/repair' do
132
+ request.body.rewind
133
+ data = request.body.read.strip
134
+ do_versions = data.pix_empty? ? false : parse_json(data)[:repair_versions]
135
+
136
+ addendum = do_versions ? ' and all versions' : ' (title only)'
137
+ log_debug "Admin #{session[:admin]} is repairing title '#{params[:title]}'#{addendum}"
138
+
139
+ halt_on_missing_title params[:title]
140
+ halt_on_locked_title params[:title]
141
+ title = instantiate_title params[:title]
142
+
143
+ with_streaming do
144
+ title.repair repair_versions: do_versions
145
+ update_client_data
146
+ end
147
+ end
148
+
149
+ # Delete an existing title
150
+ # @return [Hash] A response hash
151
+ #################################
152
+ delete '/titles/:title' do
153
+ halt_on_missing_title params[:title]
154
+
155
+ title = instantiate_title params[:title]
156
+
157
+ with_streaming do
158
+ title.delete
159
+ update_client_data
160
+ end
161
+ end
162
+
163
+ # Handle upload for self-service icon for a title
164
+ #
165
+ # @return [Hash] A response hash
166
+ #################################
167
+ post '/titles/:title/ssvc-icon' do
168
+ process_incoming_ssvc_icon
169
+ body({ result: :uploaded })
170
+ end
171
+
172
+ # Return the members of the 'frozen' static group for a title
173
+ #
174
+ # @return [Hash{String => String}] computer name => user name
175
+ #################################
176
+ get '/titles/:title/frozen' do
177
+ log_debug "Admin #{session[:admin]} is fetching frozen computers for title '#{params[:title]}'"
178
+ halt_on_missing_title params[:title]
179
+ title = instantiate_title params[:title]
180
+ body title.frozen_computers
181
+ end
182
+
183
+ # add one or more computers to the 'frozen' static group for a title
184
+ # Body should be an array of computer names
185
+ #
186
+ # @return [Hash] A response hash
187
+ #################################
188
+ put '/titles/:title/freeze' do
189
+ halt_on_missing_title params[:title]
190
+
191
+ request.body.rewind
192
+ data = parse_json(request.body.read)
193
+
194
+ comps_to_freeze = expand_freeze_thaw_targets(targets: data[:targets], users: data[:users])
195
+
196
+ log_debug "Target computers to freeze for title #{params[:title]}: #{comps_to_freeze}"
197
+ title = instantiate_title params[:title]
198
+
199
+ result = title.freeze_or_thaw_computers(action: :freeze, computers: comps_to_freeze)
200
+ body result
201
+ end
202
+
203
+ # remove one or more computers from the 'frozen' static group for a title
204
+ # Body should be an array of computer names
205
+ #
206
+ # If any computer name is 'clear_all' then all frozen computers will be thawed
207
+ #
208
+ # @return [Hash] A response hash
209
+ #################################
210
+ put '/titles/:title/thaw' do
211
+ halt_on_missing_title params[:title]
212
+
213
+ request.body.rewind
214
+ data = parse_json(request.body.read)
215
+
216
+ comps_to_thaw = expand_freeze_thaw_targets(targets: data[:targets], users: data[:users])
217
+
218
+ log_debug "Incoming computers to thaw for title #{params[:title]}: #{comps_to_thaw}"
219
+ title = instantiate_title params[:title]
220
+
221
+ result = title.freeze_or_thaw_computers(action: :thaw, computers: comps_to_thaw)
222
+ body result
223
+ end
224
+
225
+ # Return info about all the computers with a given title installed
226
+ #
227
+ # @return [Array<Hash>] The data for all computers with the given title
228
+ #################################
229
+ get '/titles/:title/patch_report' do
230
+ log_debug "Admin #{session[:admin]} is fetching patch report for title '#{params[:title]}'"
231
+ halt_on_missing_title params[:title]
232
+ title = instantiate_title params[:title]
233
+
234
+ body title.patch_report
235
+ end
236
+
237
+ # Return URLs for all the UI pages for a title
238
+ #
239
+ # @return [Hash] The URLs for all the UI pages for a title
240
+ #################################
241
+ get '/titles/:title/urls' do
242
+ log_debug "Admin #{session[:admin]} is fetching GUI URLS for title '#{params[:title]}'"
243
+ halt_on_missing_title params[:title]
244
+ title = instantiate_title params[:title]
245
+ data = {
246
+ ted_title_url: title.ted_title_url,
247
+ jamf_installed_group_url: title.jamf_installed_group_url,
248
+ jamf_frozen_group_url: title.jamf_frozen_group_url
249
+ }
250
+ data[:jamf_manual_install_released_policy_url] = title.jamf_manual_install_released_policy_url if title.jamf_manual_install_released_policy_exist?
251
+
252
+ if title.uninstallable?
253
+ data[:jamf_uninstall_script_url] = title.jamf_uninstall_script_url
254
+ data[:jamf_uninstall_policy_url] = title.jamf_uninstall_policy_url
255
+ data[:jamf_expire_policy_url] = title.jamf_expire_policy_url if title.expiration
256
+ end
257
+
258
+ if title.jamf_ted_title_active?
259
+ data[:jamf_patch_title_url] = title.jamf_patch_title_url unless title.versions.empty?
260
+ data[:jamf_patch_ea_url] = title.jamf_patch_ea_url if title.version_script
261
+ end
262
+
263
+ data[:jamf_normal_ea_url] = title.jamf_normal_ea_url if title.version_script
264
+
265
+ body data
266
+ end
267
+
268
+ # Return the changelog for a title
269
+ #
270
+ # @return [Array<Hash>] The changelog for a title
271
+ #################################
272
+ get '/titles/:title/changelog' do
273
+ log_debug "Admin #{session[:admin]} is fetching the change log for title '#{params[:title]}'"
274
+ halt_on_missing_title params[:title]
275
+ title = instantiate_title params[:title]
276
+ body title.changelog
277
+ end
278
+
279
+ end # Titles
280
+
281
+ end # Routes
282
+
283
+ end # Server
284
+
285
+ end # module Xolo
@@ -0,0 +1,93 @@
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 Uploads
18
+
19
+ # This is how we 'extend' 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
+ # Module methods
35
+ ##############################
36
+ ##############################
37
+
38
+ # when this module is included
39
+ def self.included(includer)
40
+ Xolo.verbose_include includer, self
41
+ end
42
+
43
+ # when this module is extended
44
+ def self.extended(extender)
45
+ Xolo.verbose_extend extender, self
46
+ end
47
+
48
+ # This hash contains upload progress stream urls
49
+ # for file uploads, keyed by session[:xolo_id]
50
+ # Individual sessions.
51
+ # Should be accessible from anywhere via
52
+ # Xolo::Server::App.file_upload_progress_files
53
+ # or Xolo::Server::Routes::Uploads.file_upload_progress_files
54
+ #
55
+ # @return [Hash {String => String}]
56
+ ##########################
57
+ def file_upload_progress_files
58
+ @file_upload_progress_files ||= {}
59
+ end
60
+
61
+ # Routes
62
+ ##############################
63
+ ##############################
64
+
65
+ # # param with the uploaded file must be :file
66
+ # ######################
67
+ # post '/upload/ssvc-icon/:title' do
68
+ # process_incoming_ssvc_icon
69
+ # body({ result: :uploaded })
70
+ # end
71
+
72
+ # # param with the uploaded file must be :file
73
+ # ######################
74
+ # post '/upload/pkg/:title/:version' do
75
+ # process_incoming_pkg
76
+ # body({ result: :uploaded })
77
+ # end
78
+
79
+ # param with the uploaded file must be :file
80
+ ######################
81
+ post '/upload/test' do
82
+ with_streaming do
83
+ process_incoming_testfile
84
+ end
85
+ end
86
+
87
+ end # Module
88
+
89
+ end # Routes
90
+
91
+ end # Server
92
+
93
+ end # module Xolo