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,315 @@
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 Mixins
16
+
17
+ # This is mixed in to Xolo::Server::Title and Xolo::Server::Version,
18
+ # for simplified access to a title's changelog
19
+ #
20
+ # Each title has a changelog file that records changes to the title and its versions.
21
+ #
22
+ # The changelog file is a 'jsonlines' file, which is a JSON file containing
23
+ # a single JSON object per line. See https://jsonlines.org/ for more info.
24
+ # The reason for using jsonlines is that it is easy to append to the file, rather than
25
+ # having to read the whole file into memory, parse it, add a new entry, and write it back.
26
+ #
27
+ # In this case, each line is a JSON object (ruby Hash) representing a change or an action.
28
+ #
29
+ # The keys in the hash are:
30
+ # :time - the time the change was made
31
+ # :admin - the admin who made the change
32
+ # :host - the hostname or IP address of the admin
33
+ # :version - the version number, or nil if the change is to the title
34
+ # :attrib - the attribute name, or nil if the change is an action
35
+ # :old - the original value, or nil if the change is an action
36
+ # :new - the new value, or nil if the change is an action
37
+ # :action - a description of the action, or nil if the change is to an attribute
38
+ #
39
+ # The changelog file is stored in the title directory in a file named 'changelog.json'.
40
+ # The file exists for as long as the title exists.
41
+ # It is backed up when before every event logged to it, in the backup directory in
42
+ # the server's BACKUPS_DIR.
43
+ #
44
+ # When a title is deleted, its changelog file is moved to a backup directory before
45
+ # the title directory is deleted, and will remain there until manually removed.
46
+ #
47
+ module Changelog
48
+
49
+ # Constants
50
+ #######################
51
+ #######################
52
+
53
+ # The change log filename
54
+ TITLE_CHANGELOG_FILENAME = 'changelog.jsonl'
55
+
56
+ # Module Methods
57
+ #######################
58
+ #######################
59
+
60
+ # when this module is included
61
+ def self.included(includer)
62
+ Xolo.verbose_include includer, self
63
+ end
64
+
65
+ # When a title is deleted, its changelog is moved to this directory and
66
+ # renamed to '<title>_changelog.json'
67
+ # This is so that the changelog can be accessed after the title is deleted.
68
+ ################
69
+ def self.backup_file_dir
70
+ @backup_file_dir ||= Xolo::Server::BACKUPS_DIR + 'changelogs'
71
+ end
72
+
73
+ # A hash of the read-write locks for each title's changelog file
74
+ # The key is the title name, the value is the Concurrent::ReentrantReadWriteLock
75
+ # instance for that title.
76
+ #
77
+ # Titles and versions use these locks to ensure that only one
78
+ # thread at a time can write to a title's changelog file.
79
+ #
80
+ # @return [Concurrent::Hash] the locks
81
+ def self.changelog_locks
82
+ @changelog_locks ||= Concurrent::Hash.new
83
+ end
84
+
85
+ # Instance Methods
86
+ #######################
87
+ #######################
88
+
89
+ # the change log file for a title
90
+ #
91
+ # @param title [String] the title
92
+ #
93
+ # @return [Pathname] the path to the file
94
+ #######################
95
+ def changelog_file
96
+ @changelog_file ||= Xolo::Server::Title.title_dir(title) + TITLE_CHANGELOG_FILENAME
97
+ end
98
+
99
+ # @return [Pathname] the path to the backup file for this title's changelog
100
+ #######################
101
+ def changelog_backup_file
102
+ @changelog_backup_file ||= Xolo::Server::Mixins::Changelog.backup_file_dir + "#{title}-#{TITLE_CHANGELOG_FILENAME}"
103
+ end
104
+
105
+ # the read-write lock for a title's changelog file
106
+ #
107
+ # @param title [String] the title
108
+ #
109
+ # @return [Concurrent::ReentrantReadWriteLock] the lock
110
+ #######################
111
+ def changelog_lock
112
+ @changelog_lock ||=
113
+ if Xolo::Server::Mixins::Changelog.changelog_locks[title]
114
+ Xolo::Server::Mixins::Changelog.changelog_locks[title]
115
+ else
116
+ log_debug "Creating changelog lock for #{title}"
117
+ Xolo::Server::Mixins::Changelog.changelog_locks[title] = Concurrent::ReentrantReadWriteLock.new
118
+ end
119
+ end
120
+
121
+ # the change log for a title
122
+ #
123
+ # @return [Array<Hash>] the changelog
124
+ #######################
125
+ def changelog
126
+ log_debug "Reading changelog for #{title}"
127
+ changelog_data = []
128
+
129
+ if changelog_file.exist?
130
+ changelog_lock.with_read_lock do
131
+ changelog_file.read.lines.each { |l| changelog_data << JSON.parse(l, symbolize_names: true) }
132
+ end
133
+ end
134
+
135
+ # reverse the order so the most recent change is first
136
+ changelog_data.reverse
137
+ end
138
+
139
+ # Copy the changelog file to the backup directory
140
+ #
141
+ # @return [void]
142
+ #######################
143
+ def backup_changelog
144
+ return unless changelog_file.exist?
145
+
146
+ unless changelog_backup_file.dirname.exist?
147
+ log_debug 'Creating backup directory for changelogs'
148
+ changelog_backup_file.dirname.mkpath
149
+ end
150
+
151
+ log_debug "Backing up changelog for #{title}"
152
+
153
+ if changelog_backup_file.exist?
154
+ # if deleting the whole title
155
+ # move aside any previously existing one, appending a timestamp
156
+ if self.class == Xolo::Server::Title && deleting?
157
+ changelog_backup_file.rename "#{changelog_backup_file.basename}.#{changelog_backup_file.mtime.strftime('%Y%m%d%H%M%S')}"
158
+
159
+ # otherwise, overwrite the current backup
160
+ else
161
+ changelog_backup_file.delete
162
+ end
163
+
164
+ end
165
+ changelog_file.pix_cp changelog_backup_file
166
+ end
167
+
168
+ # Log a change by adding an entry to the changelog file for a title
169
+ # or one of its versions.
170
+ #
171
+ # The entry may be for an message, such as 'Title Created',
172
+ # or for a change to the value of an attribute.
173
+ #
174
+ # Provide either a message to log with :msg,
175
+ # or the name of an attribute being changed, with :attrib,
176
+ # and either :old_val, :new_val, or both.
177
+ # (either can be omitted or set to nil, when adding or removing the attribute)
178
+ #
179
+ # @param attrib [Symbol] the attribute name
180
+ # @param old_val [Object] the original value
181
+ # @param new_val [Object] the new value
182
+ # @param msg [String] an arbitrary message to log
183
+ #
184
+ # @return [void]
185
+ #######################
186
+ def log_change(attrib: nil, old_val: nil, new_val: nil, msg: nil)
187
+ raise ArgumentError, 'Must provide attrib: or action:' if !msg && !attrib
188
+ raise ArgumentError, 'Must provide old: or new: or both with attrib:' if attrib && !old_val && !new_val
189
+
190
+ # if action, attrib, old, and new are ignored
191
+ attrib, old_val, new_val = nil if msg
192
+
193
+ change = {
194
+ time: Time.now,
195
+ admin: session[:admin],
196
+ host: hostname_from_ip(server_app_instance.request.ip),
197
+ version: respond_to?(:version) ? version : nil,
198
+ msg: msg,
199
+ attrib: attrib,
200
+ old: old_val,
201
+ new: new_val
202
+ }
203
+
204
+ log_debug "Writing to changelog for #{title}"
205
+
206
+ changelog_lock.with_write_lock do
207
+ backup_changelog
208
+ changelog_file.pix_append "#{change.to_json}\n"
209
+ end
210
+ end
211
+
212
+ # get a hostname from an IP address if possible
213
+ #
214
+ # @param ip [String] the IP address
215
+ #
216
+ # @return [String] the hostname or the IP address if the hostname cannot be found
217
+ #######################
218
+ def hostname_from_ip(ip)
219
+ # gethostbbaddr is deprecated, so use Resolv instead
220
+ # host = Socket.gethostbyaddr(ip.split('.').map(&:to_i).pack('CCCC')).first
221
+
222
+ host = Resolv.getname(ip)
223
+
224
+ host.pix_empty? ? ip : host
225
+ rescue Resolv::ResolvError
226
+ ip
227
+ end
228
+
229
+ # At the start of an update, populate the hash for the @changes_for_update attribute
230
+ # with the changes being made.
231
+ #
232
+ # This is run at the start of the update process, and
233
+ #
234
+ # @return [Hash] The changes being made
235
+ def note_changes_for_update_and_log
236
+ return unless new_data_for_update
237
+
238
+ changes = {}
239
+
240
+ self.class::ATTRIBUTES.each do |attr, deets|
241
+ next unless deets[:changelog]
242
+
243
+ new_val = deets[:type] == :time ? Time.parse(new_data_for_update[attr]) : new_data_for_update[attr]
244
+ old_val = send attr
245
+
246
+ # Don't change arrays to strings!
247
+ # just sort them to compare
248
+ new_val_to_compare = new_val.is_a?(Array) ? new_val.sort : new_val
249
+ old_val_to_compare = old_val.is_a?(Array) ? old_val.sort : old_val
250
+ next if new_val_to_compare == old_val_to_compare
251
+
252
+ changes[attr] = { old: old_val, new: new_val }
253
+ end
254
+
255
+ changes
256
+ end
257
+
258
+ # Record all changes during an update of a title or version
259
+ #
260
+ # @return [void]
261
+ #######################
262
+ def log_update_changes
263
+ return unless changes_for_update
264
+
265
+ # self.class::ATTRIBUTES.each do |attr, deets|
266
+ # next unless deets[:changelog]
267
+
268
+ # new_val = deets[:type] == :time ? Time.parse(new_data_for_update[attr]) : new_data_for_update[attr]
269
+ # old_val = send attr
270
+
271
+ # new_val = "'#{new_val.sort.join("', '")}'" if new_val.is_a? Array
272
+ # old_val = "'#{old_val.sort.join("', '")}'" if old_val.is_a? Array
273
+ # next if new_val == old_val
274
+
275
+ # log_change attrib: attr, old_val: old_val, new_val: new_val
276
+ # end
277
+
278
+ changes_for_update.each do |attr, vals|
279
+ log_change attrib: attr, old_val: vals[:old], new_val: vals[:new]
280
+ end
281
+ end
282
+
283
+ # when a title is deleted, make a final entry, then
284
+ # move its changelog to the backup directory
285
+ #
286
+ # @return [void]
287
+ #######################
288
+ def delete_changelog
289
+ change = {
290
+ time: Time.now,
291
+ admin: session[:admin],
292
+ host: hostname_from_ip(server_app_instance.request.ip),
293
+ version: nil,
294
+ action: 'Title Deleted',
295
+ attrib: nil,
296
+ old: nil,
297
+ new: nil
298
+ }
299
+
300
+ changelog_lock.with_write_lock do
301
+ changelog_file.pix_append "#{change.to_json}\n"
302
+
303
+ # final backup
304
+ changelog_backup_file.delete if changelog_backup_file.exist?
305
+ changelog_file.rename changelog_backup_file
306
+ end
307
+ end
308
+
309
+ end # Changelog
310
+
311
+ end # Mixins
312
+
313
+ end # Server
314
+
315
+ end # module Xolo