xolo-server 1.0.1 → 2.0.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/data/client/xolo +152 -79
  3. data/lib/xolo/core/base_classes/title.rb +254 -18
  4. data/lib/xolo/core/base_classes/version.rb +47 -7
  5. data/lib/xolo/core/constants.rb +7 -3
  6. data/lib/xolo/core/security_cmd.rb +128 -0
  7. data/lib/xolo/core/version.rb +1 -1
  8. data/lib/xolo/core.rb +1 -0
  9. data/lib/xolo/server/app.rb +7 -0
  10. data/lib/xolo/server/configuration.rb +243 -37
  11. data/lib/xolo/server/constants.rb +10 -0
  12. data/lib/xolo/server/helpers/auth.rb +19 -2
  13. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  14. data/lib/xolo/server/helpers/client_data.rb +90 -60
  15. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  16. data/lib/xolo/server/helpers/jamf_pro.rb +30 -7
  17. data/lib/xolo/server/helpers/log.rb +2 -0
  18. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  19. data/lib/xolo/server/helpers/notification.rb +4 -3
  20. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  21. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  22. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  23. data/lib/xolo/server/helpers/titles.rb +27 -3
  24. data/lib/xolo/server/helpers/versions.rb +23 -11
  25. data/lib/xolo/server/mixins/changelog.rb +9 -16
  26. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -385
  27. data/lib/xolo/server/mixins/title_ted_access.rb +29 -3
  28. data/lib/xolo/server/mixins/version_jamf_access.rb +95 -112
  29. data/lib/xolo/server/mixins/version_ted_access.rb +25 -0
  30. data/lib/xolo/server/object_locks.rb +2 -1
  31. data/lib/xolo/server/routes/auth.rb +2 -2
  32. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  33. data/lib/xolo/server/routes/maint.rb +2 -1
  34. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  35. data/lib/xolo/server/routes/title_editor.rb +1 -1
  36. data/lib/xolo/server/routes/titles.rb +26 -11
  37. data/lib/xolo/server/routes/uploads.rb +0 -14
  38. data/lib/xolo/server/routes/versions.rb +14 -13
  39. data/lib/xolo/server/routes.rb +9 -0
  40. data/lib/xolo/server/title.rb +100 -77
  41. data/lib/xolo/server/version.rb +177 -15
  42. data/lib/xolo/server.rb +8 -0
  43. metadata +7 -9
@@ -223,12 +223,86 @@ module Xolo
223
223
  }
224
224
  end
225
225
 
226
+ # add a new version in response to a patch title update webhook event.
227
+ # This doesn't upload a pkg - it just creates the version in Xolo, and then
228
+ # someone can upload a pkg to it via xadm or autopkg will do it if configured.
229
+ #
230
+ # @param title_object [Xolo::Server::Title] the title object for the subscribed title
231
+ # @param new_version [String] the new version to add
232
+ # @return [void]
233
+ ######################
234
+ def self.add_version_via_subscription(title_object:, new_version:)
235
+ title_object.log_info "Adding new version '#{new_version}' for subscribed title '#{title_object.title}'"
236
+
237
+ # get more details about this version from the JPAPI
238
+ patch_version_data = title_object.patch_versions(version: new_version).first
239
+ unless patch_version_data
240
+ msg = "Could not get patch version data from JPAPI for version '#{new_version}' of subscribed title '#{title_object.title}'. Cannot create new version in Xolo without this data. Aborting."
241
+ title_object.log_error msg, alert: true
242
+ return
243
+ end
244
+
245
+ title_object.log_debug "Got patch version data from JPAPI for version '#{new_version}': #{patch_version_data}"
246
+
247
+ # put the data into a hash for creating a new version object
248
+ vobj_data = {
249
+ publish_date: Time.parse(patch_version_data[:releaseDate]),
250
+ standalone: patch_version_data[:standalone],
251
+ min_os: patch_version_data[:minimumOperatingSystem],
252
+ reboot: patch_version_data[:rebootRequired],
253
+ killapps: []
254
+ }
255
+
256
+ # Killapps for subscribed titles? The API only shows app names without the .app, e.g.
257
+ # "killApps": [
258
+ # {
259
+ # "appName": "ChrislTestHelper"
260
+ # },
261
+ # {
262
+ # "appName": "Chrisl Test"
263
+ # }
264
+ # ]
265
+ # Since we don't manage them, we'll just record them in the data like this...
266
+ unless patch_version_data[:killApps].pix_empty?
267
+ patch_version_data[:killApps].each do |ka|
268
+ vobj_data[:killapps] << "#{ka[:appName]}.app;unknown.from.subscription"
269
+ end
270
+ end
271
+
272
+ # instantiate the version object
273
+ title_object.log_debug "Instantiating version via subscription '#{new_version}' of title '#{title_object.title}' (#{title_object.class}) with data: #{vobj_data}"
274
+
275
+ vobj = title_object.server_app_instance.instantiate_version(
276
+ title: title_object,
277
+ version: new_version,
278
+ **vobj_data
279
+ )
280
+
281
+ # create it in xolo
282
+ vobj.create
283
+
284
+ # tell someone
285
+ msg = "ACTION REQUIRED: New pilot version '#{new_version}' for subscribed title '#{title_object.title}' has been created in Xolo via subscription."
286
+
287
+ # if not autopkg enabled, we need to tell someone to upload a pkg for this new version
288
+ unless title_object.autopkg_enabled?
289
+ # update general alert msg
290
+ msg = "#{msg}\nPlease upload a .pkg for it ASAP using this command:\n xadm edit-version #{title_object.title} #{new_version} --pkg-to-upload /path/to/installer.pkg"
291
+
292
+ # email to title contact
293
+ vobj.server_app_instance.send_email to: title_object.contact_email, subject: 'Need manual upload of xolo pkg', msg: msg
294
+ end # if title_object.autopkg_enabled?
295
+
296
+ # send general alert
297
+ vobj.log_info msg, alert: true
298
+ end
299
+
226
300
  # Attributes
227
301
  ######################
228
302
  ######################
229
303
 
230
304
  # The instance of Xolo::Server::App that instantiated this
231
- # title object. This is how we access things that are available in routes
305
+ # version object. This is how we access things that are available in routes
232
306
  # and helpers, like the single Jamf and TEd
233
307
  # connections for this App instance.
234
308
  attr_accessor :server_app_instance
@@ -297,6 +371,9 @@ module Xolo
297
371
  # one of :creating, :updating, :deleting
298
372
  attr_accessor :current_action
299
373
 
374
+ # @return [Boolean] is the pkg being processed now from an autopkg recipe?
375
+ attr_accessor :pkg_is_from_autopkg
376
+
300
377
  # Constructor
301
378
  ######################
302
379
  ######################
@@ -314,15 +391,15 @@ module Xolo
314
391
  @jamf_pkg_id ||= data_hash[:jamf_pkg_id]
315
392
 
316
393
  # and these can be generated now
317
- @jamf_obj_name_pfx = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{title}-#{version}"
394
+ @jamf_obj_name_pfx = "#{jamf_obj_name_pfx_base}#{title}-#{version}"
318
395
 
319
396
  @jamf_pkg_name ||= @jamf_obj_name_pfx
320
397
 
321
- @jamf_installed_group_name = "#{jamf_obj_name_pfx}#{JAMF_SMART_GROUP_NAME_INSTALLED_SFX}"
398
+ @jamf_installed_group_name = "#{jamf_obj_name_pfx}-#{JAMF_SMART_GROUP_NAME_INSTALLED_SFX}"
322
399
 
323
- @jamf_auto_install_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_AUTO_INSTALL_SFX}"
324
- @jamf_manual_install_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_MANUAL_INSTALL_SFX}"
325
- @jamf_auto_reinstall_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_AUTO_REINSTALL_SFX}"
400
+ @jamf_auto_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_INSTALL_SFX}"
401
+ @jamf_manual_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_MANUAL_INSTALL_SFX}"
402
+ @jamf_auto_reinstall_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_REINSTALL_SFX}"
326
403
 
327
404
  @jamf_patch_policy_name = @jamf_obj_name_pfx
328
405
 
@@ -391,7 +468,17 @@ module Xolo
391
468
  def pilot_groups_to_use
392
469
  return @pilot_groups_to_use if @pilot_groups_to_use
393
470
 
471
+ # any defined in the version override any in the title
394
472
  @pilot_groups_to_use = changes_for_update&.key?(:pilot_groups) ? changes_for_update[:pilot_groups][:new] : pilot_groups
473
+ return @pilot_groups_to_use unless @pilot_groups_to_use.empty?
474
+
475
+ # if none defined in the version, look in the title
476
+ @pilot_groups_to_use =
477
+ if title_object.changes_for_update&.key?(:pilot_groups)
478
+ title_object.changes_for_update[:pilot_groups][:new]
479
+ else
480
+ title_object.pilot_groups
481
+ end
395
482
  end
396
483
 
397
484
  # The scope excluded groups to use in policies and patch policies for all versions of
@@ -456,10 +543,12 @@ module Xolo
456
543
  # @session ||= {}
457
544
  end
458
545
 
546
+ # This can be manually set earlier in the request handling to use a non-standard
547
+ # admin username
459
548
  # @return [String]
460
549
  ###################
461
550
  def admin
462
- session[:admin]
551
+ @admin ||= session[:admin]
463
552
  end
464
553
 
465
554
  # Append a message to the progress stream file,
@@ -472,8 +561,8 @@ module Xolo
472
561
  #
473
562
  # @return [void]
474
563
  ###################
475
- def progress(msg, log: :debug)
476
- server_app_instance.progress msg, log: log
564
+ def progress(msg, log: :debug, alert: false)
565
+ server_app_instance.progress msg, log: log, alert: alert
477
566
  end
478
567
 
479
568
  # This might have been set already if we were instantiated via our title
@@ -559,7 +648,7 @@ module Xolo
559
648
  progress 'Saving version data to Xolo server'
560
649
  save_local_data
561
650
 
562
- create_patch_in_ted
651
+ create_patch_in_ted unless subscribed?
563
652
 
564
653
  create_in_jamf
565
654
 
@@ -576,10 +665,68 @@ module Xolo
576
665
  log_change msg: 'Version Created'
577
666
 
578
667
  progress "Version '#{version}' of Title '#{title}' has been created in Xolo.", log: :info
668
+
669
+ # all done unless we need to get a pkg via autopkg
670
+ # pkg upload from xadm will happen in a separate process,
671
+ # so we don't want to do it here in the create method
672
+
673
+ # do we have an uploaded pkg?
674
+ if pkg_to_upload.to_s.start_with? '/'
675
+ progress "Pkg will be uploaded to xolo via xadm shortly, from path '#{pkg_to_upload}'", log: :info
676
+
677
+ # if we have an autopkg recipe and dir, get the .pkg and upload it to Jamf
678
+ elsif title_object.autopkg_enabled?
679
+ handle_autopkg_during_create
680
+
681
+ # otherwise tell someone we need a .pkg
682
+ else
683
+ msg = "No --pkg-to-upload given for version '#{version}' of title #{title}, and no autopkg recipe enabled. Please upload a pkg via xadm or enable autopkg for this title."
684
+
685
+ # no alert when subscribed because a better alert mesg is sent from add_version_via_subscription
686
+ subscribed? ? progress(msg, log: :warn) : progress(msg, log: :warn, alert: true)
687
+ end
579
688
  ensure
580
689
  unlock
581
690
  end
582
691
 
692
+ # Do autopkg stuff during creation
693
+ #
694
+ ############################
695
+ def handle_autopkg_during_create
696
+ return unless title_object.autopkg_enabled?
697
+
698
+ pkg_src = title_object.run_autopkg_recipe
699
+
700
+ if pkg_src.nil?
701
+ msg = 'AutoPkg recipe is enabled for this title, but no pkg was found after running the recipe. Please check the AutoPkg recipe and the server log for details.'
702
+ progress msg, log: :warn, alert: true
703
+ return
704
+ end
705
+
706
+ oldest_allowed = Time.now - 1200 # 20 minutes ago
707
+ if pkg_src.mtime < oldest_allowed
708
+ msg = "AutoPkg recipe is enabled for this title, and a pkg was found after running the recipe, but it was last modified at #{pkg_src.mtime}, which is more than 20 minutes ago. To avoid accidentally uploading an old pkg, the server will not upload this pkg. Please check the AutoPkg recipe and the server log for details."
709
+ progress msg, log: :warn, alert: true
710
+ return
711
+ end
712
+
713
+ # this lets future code know that the pkg we're working with came from autopkg
714
+ self.pkg_is_from_autopkg = true
715
+
716
+ # Upload the pkg to Jamf, and associate it with this version
717
+ server_app_instance.process_and_upload_autopkg_pkg(title, self, pkg_src)
718
+ end
719
+
720
+ # Is this version part of a subscribed title?
721
+ def subscribed?
722
+ title_object.subscribed?
723
+ end
724
+
725
+ # or a managed title?
726
+ def managed?
727
+ !subscribed?
728
+ end
729
+
583
730
  # Update a this version, updating to the
584
731
  # local filesystem, Jamf Pro, and the Title Editor as needed
585
732
  #
@@ -666,13 +813,16 @@ module Xolo
666
813
  progress "Fixing original upload date: #{new_date}, by: #{new_by}", log: :debug
667
814
  self.upload_date = new_date
668
815
  self.uploaded_by = new_by
669
- save_local_data
816
+
670
817
  end
818
+ save_local_data
671
819
  ensure
672
820
  unlock
673
821
  end
674
822
 
675
823
  # Release this version, possibly rolling back from a previously newer version
824
+ # This should only be called by the title. The initial 'release' action starts in the title,
825
+ # and then calls this method on the version to do the version-specific release steps.
676
826
  #
677
827
  # @param rollback [Boolean] If true, this version is being released as a rollback
678
828
  #
@@ -823,15 +973,25 @@ module Xolo
823
973
  # know the version is gone. Set this to false when the title itself
824
974
  # is being deleted and calling this method.
825
975
  #
976
+ # @param deleting_title [Boolean] Is the title itself being deleted?
977
+ #
826
978
  # @return [void]
827
979
  ##########################
828
- def delete(update_title: true)
980
+ def delete(update_title: true, deleting_title: false)
829
981
  lock
830
982
  @current_action = :deleting
831
983
 
832
- delete_patch_from_ted
833
984
  delete_version_from_jamf
834
985
 
986
+ # NOTE: we no longer delete the patch from the Title Editor
987
+ # unless the whole title is being deleted, because
988
+ # patches may be needed for reporting purposes.
989
+ # When the title is deleted, the title's delete method
990
+ # will delete all patches for all versions.
991
+ # If other situations arise where we need to delete
992
+ # ted patches individually, set deleting_title to true.
993
+ delete_patch_from_ted if deleting_title && managed?
994
+
835
995
  # remove from the title's list of versions
836
996
  progress 'Deleting version from title data on the Xolo server', log: :debug
837
997
  title_object.remove_version(version) if update_title
@@ -858,14 +1018,16 @@ module Xolo
858
1018
  raise Xolo::ServerError, 'Server is shutting down' if Xolo::Server.shutting_down?
859
1019
 
860
1020
  while locked?
861
- log_debug "Waiting for update lock on Version '#{version}' of title '#{title}'..." if (Time.now.to_i % 5).zero?
1021
+ if (Time.now.to_i % 5).zero?
1022
+ log_debug "Method #{caller_locations.first.label} is waiting for update lock on Version '#{version}' of title '#{title}'..."
1023
+ end
862
1024
  sleep 0.33
863
1025
  end
864
1026
  Xolo::Server.object_locks[title] ||= { versions: {} }
865
1027
 
866
1028
  exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
867
1029
  Xolo::Server.object_locks[title][:versions][version] = exp
868
- log_debug "Locked version '#{version}' of title '#{title}' for updates until #{exp}"
1030
+ log_debug "Locked version '#{version}' of title '#{title}' for updates until #{exp}, by method #{caller_locations.first.label}"
869
1031
  end
870
1032
 
871
1033
  # Unlock this version for updates
data/lib/xolo/server.rb CHANGED
@@ -27,6 +27,7 @@ require 'base64'
27
27
  require 'resolv'
28
28
  require 'shellwords'
29
29
  require 'net/smtp'
30
+ require 'etc'
30
31
 
31
32
  # Gems
32
33
  ######
@@ -102,6 +103,11 @@ module Xolo
102
103
  ##############################
103
104
  ##############################
104
105
 
106
+ ################
107
+ def self.test_server?
108
+ Xolo::Server.config.test_server
109
+ end
110
+
105
111
  ################
106
112
  def self.start_time
107
113
  @start_time
@@ -194,6 +200,8 @@ require 'xolo/server/helpers/versions'
194
200
  require 'xolo/server/helpers/client_data'
195
201
  require 'xolo/server/helpers/file_transfers'
196
202
  require 'xolo/server/helpers/maintenance'
203
+ require 'xolo/server/helpers/subscriptions'
204
+ require 'xolo/server/helpers/autopkg'
197
205
 
198
206
  require 'xolo/server/configuration'
199
207
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xolo-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Lasell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-03 00:00:00.000000000 Z
11
+ date: 2026-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -152,14 +152,8 @@ dependencies:
152
152
  version: '1.0'
153
153
  description: |
154
154
  == Xolo
155
- Xolo (sorta pronounced 'show-low') is an HTTPS server and set of command-line tools for macOS that provide automatable access to the software deployment and patch management aspects of {Jamf Pro}[https://www.jamf.com/products/jamf-pro/] and the {Jamf Title Editor}[https://learn.jamf.com/en-US/bundle/title-editor/page/About_Title_Editor.html]. It enhances Jamf Pro's abilities in many ways:
155
+ Xolo (sorta pronounced 'show-low') is an HTTPS server and set of command-line tools for macOS that provide automatable access to the software deployment and patch management aspects of {Jamf Pro}[https://www.jamf.com/products/jamf-pro/] and the {Jamf Title Editor}[https://learn.jamf.com/en-US/bundle/title-editor/page/About_Title_Editor.html]. It enhances Jamf Pro's abilities in many ways.
156
156
 
157
- * Management of titles and versions/patches is scriptable and automatable, allowing developers and admins to integrate with CI/CD workflows.
158
- * Simplifies and standardizes the complex, multistep manual process of managing titles and patches using the Title Editor and Patch Management web interfaces.
159
- * Client installs can be performed by remotely via ssh and/or MDM
160
- * Automated pre-release piloting of new versions/patches
161
- * Titles can be expired (auto-uninstalled) after a period of disuse, reclaiming unused licenses.
162
- * And more!
163
157
 
164
158
  The xolo-server gem packages the code needed to run `xoloserver`, the sinatra-based HTTPS server at the heart of Xolo.
165
159
  email: xolo@pixar.com
@@ -188,6 +182,7 @@ files:
188
182
  - lib/xolo/core/json_wrappers.rb
189
183
  - lib/xolo/core/loading.rb
190
184
  - lib/xolo/core/output.rb
185
+ - lib/xolo/core/security_cmd.rb
191
186
  - lib/xolo/core/version.rb
192
187
  - lib/xolo/server.rb
193
188
  - lib/xolo/server/app.rb
@@ -195,6 +190,7 @@ files:
195
190
  - lib/xolo/server/configuration.rb
196
191
  - lib/xolo/server/constants.rb
197
192
  - lib/xolo/server/helpers/auth.rb
193
+ - lib/xolo/server/helpers/autopkg.rb
198
194
  - lib/xolo/server/helpers/client_data.rb
199
195
  - lib/xolo/server/helpers/file_transfers.rb
200
196
  - lib/xolo/server/helpers/jamf_pro.rb
@@ -203,6 +199,7 @@ files:
203
199
  - lib/xolo/server/helpers/notification.rb
204
200
  - lib/xolo/server/helpers/pkg_signing.rb
205
201
  - lib/xolo/server/helpers/progress_streaming.rb
202
+ - lib/xolo/server/helpers/subscriptions.rb
206
203
  - lib/xolo/server/helpers/title_editor.rb
207
204
  - lib/xolo/server/helpers/titles.rb
208
205
  - lib/xolo/server/helpers/versions.rb
@@ -217,6 +214,7 @@ files:
217
214
  - lib/xolo/server/routes/auth.rb
218
215
  - lib/xolo/server/routes/jamf_pro.rb
219
216
  - lib/xolo/server/routes/maint.rb
217
+ - lib/xolo/server/routes/subscriptions.rb
220
218
  - lib/xolo/server/routes/title_editor.rb
221
219
  - lib/xolo/server/routes/titles.rb
222
220
  - lib/xolo/server/routes/uploads.rb