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,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
|