Class: Xolo::Server::Version

Overview

Xolo Version/Patch as used on the Xolo Server

The code in this file mostly deals with the data on the Xolo server itself, and general methods for manipulating the version.

Code for interacting with the Title Editor and Jamf Pro are in the helpers and mixins.

NOTE be sure to only instantiate these using the server’s ‘instantiate_version’ method, or else they might not have all the correct innards

Constant Summary collapse

VERSIONS_DIRNAME =

On the server, xolo versions are represented by JSON files in the ‘versions’ directory of the title directory

So a title ‘foobar’ would have a directory

(Xolo::Server::DATA_DIR)/titles/foobar/

In there will be a ‘versions’ dir containing json files for each version of the title.

'versions'
JAMF_PKG_NOTES_VERS_PH =
'XOLO-VERSION-HERE'
JAMF_PKG_NOTES_TITLE_PH =
'XOLO-TITLE-HERE'
JAMF_PKG_NOTES_PREFIX =

The ‘Notes’ of a jamf pkg are the Xolo Title Description, with this prepended

<<~ENDNOTES
  This package is maintained by 'xolo', to install version '#{JAMF_PKG_NOTES_VERS_PH}' of title '#{JAMF_PKG_NOTES_TITLE_PH}'. The description in Xolo is:


ENDNOTES
MAX_PKG_DELETION_THREADS =
10
STUB_PATCH_VERSION =

STUB PATCH

We create a fake ‘stub’ patch with all ted titles so that we can activate the title before any real version is added and also accept any EA/version_script, either manually or automatically

This version should never be available to any mac, and needs no patch policies or packages.

It should also never be deleted until the title itself is deleted.

'0.0.0x0'
STUB_PATCH_CAPABILITY_CRITERION_NAME =

machines that can install this version

'Operating System Version'
STUB_PATCH_CAPABILITY_CRITERION_OPERATOR =
'less than or equal'
STUB_PATCH_CAPABILITY_CRITERION_VALUE =
'10.0'
STUB_PATCH_COMPONENT_NAME =

machines that have this version installed

'Xolo Stub'
STUB_PATCH_COMPONENT_CRITERION_NAME =
'Application Title'
STUB_PATCH_COMPONENT_CRITERION_OPERATOR =
'is'
STUB_PATCH_COMPONENT_CRITERION_VALUE =
'XoloStub-DoesNotExist.app'

Constants included from Mixins::VersionJamfAccess

Mixins::VersionJamfAccess::JAMF_POLICY_NAME_AUTO_INSTALL_SFX, Mixins::VersionJamfAccess::JAMF_POLICY_NAME_AUTO_REINSTALL_SFX, Mixins::VersionJamfAccess::JAMF_POLICY_NAME_MANUAL_INSTALL_SFX, Mixins::VersionJamfAccess::JAMF_SMART_GROUP_NAME_INSTALLED_SFX

Constants included from Mixins::Changelog

Mixins::Changelog::TITLE_CHANGELOG_FILENAME

Constants included from Helpers::Notification

Helpers::Notification::ALERT_TOOL_EMAIL_PREFIX, Helpers::Notification::DFT_EMAIL_FROM

Constants included from Helpers::JamfPro

Helpers::JamfPro::PATCH_REPORT_JPAPI_PAGE_SIZE, Helpers::JamfPro::PATCH_REPORT_UNKNOWN_VERSION

Constants inherited from Core::BaseClasses::Version

Core::BaseClasses::Version::ATTRIBUTES, Core::BaseClasses::Version::DEFAULT_MIN_OS, Core::BaseClasses::Version::STATUS_DEPRECATED, Core::BaseClasses::Version::STATUS_PENDING, Core::BaseClasses::Version::STATUS_PILOT, Core::BaseClasses::Version::STATUS_RELEASED, Core::BaseClasses::Version::STATUS_SKIPPED, Core::BaseClasses::Version::USE_TITLE_FOR_KILLAPP

Instance Attribute Summary collapse

Attributes inherited from Core::BaseClasses::Version

#created_by, #creation_date, #deployed_by, #deprecated_by, #deprecation_date, #dist_pkg, #jamf_pkg, #jamf_pkg_file, #jamf_pkg_name, #modification_date, #modified_by, #release_date, #reupload_date, #reuploaded_by, #sha_512, #skipped_by, #skipped_date, #status, #upload_date, #uploaded_by

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mixins::VersionTedAccess

#create_patch_in_ted, #delete_patch_from_ted, #enable_ted_patch, #get_patch_component_criteria_params, included, #repair_ted_patch, #set_app_component, #set_ea_component, #set_patch_capabilites, #set_patch_killapps, #set_ted_patch_component_criteria, #ted_patch_url, #update_patch_in_ted

Methods included from Mixins::VersionJamfAccess

#activate_managed_patch_version_in_jamf, #activate_patch_version_in_jamf, #activate_subscribed_patch_version_in_jamf, #assign_pkg_to_patch_in_jamf, #configure_jamf_auto_install_policy, #configure_jamf_auto_reinstall_policy, #configure_jamf_installed_group, #configure_jamf_manual_install_policy, #create_in_jamf, #create_jamf_auto_install_policy, #create_jamf_auto_reinstall_policy, #create_jamf_installed_group, #create_jamf_manual_install_policy, #create_jamf_package, #create_jamf_patch_policy, #delete_jamf_package, #delete_version_from_jamf, #deploy_via_mdm, #disable_policies_for_deprecation_or_skipping, #expand_groups_for_deploy, extended, included, #jamf_auto_install_policy, #jamf_auto_install_policy_exist?, #jamf_auto_install_policy_url, #jamf_auto_reinstall_policy, #jamf_auto_reinstall_policy_exist?, #jamf_auto_reinstall_policy_url, #jamf_gui_url, #jamf_installed_group, #jamf_installed_group_criteria, #jamf_installed_group_exist?, #jamf_installed_group_url, #jamf_manual_install_policy, #jamf_manual_install_policy_exist?, #jamf_manual_install_policy_url, #jamf_package, #jamf_package_exist?, #jamf_package_notes, #jamf_package_url, #jamf_patch_policy, #jamf_patch_policy_exist?, #jamf_patch_policy_url, #jamf_patch_version, #patch_report, #remove_exclusions_from_deploy, #remove_invalid_computers_for_deploy, #repair_jamf_auto_install_policy, #repair_jamf_auto_reinstall_policy, #repair_jamf_installed_group, #repair_jamf_manual_install_policy, #repair_jamf_package, #repair_jamf_patch_policy, #repair_jamf_version_objects, #reset_policies_to_pilot, #set_policy_exclusions, #set_policy_pilot_groups, #set_policy_release_groups, #set_policy_to_all_targets, #update_excluded_groups, #update_jamf_package_notes, #update_jamf_pkg_min_os, #update_jamf_pkg_reboot, #update_pilot_groups, #update_release_groups, #update_version_in_jamf

Methods included from Mixins::Changelog

#backup_changelog, backup_file_dir, #changelog, #changelog_backup_file, #changelog_file, #changelog_lock, changelog_locks, #delete_changelog, #hostname_from_ip, included, #log_change, #log_update_changes, #note_changes_for_update_and_log

Methods included from Helpers::Notification

#email_from, included, #send_alert, #send_email, #send_email_alert, #server_fqdn, #server_name

Methods included from Helpers::Log

included, #log_debug, #log_error, #log_fatal, #log_info, #log_unknown, #log_warn, #logger, #session_svr_obj_id

Methods included from Helpers::TitleEditor

included

Methods included from Helpers::JamfPro

extended, included, #jamf_gui_url, #jamf_obj_name_pfx_base, #jamf_xolo_category_id, #valid_forced_exclusion_group_name

Methods inherited from Core::BaseClasses::Version

#deprecated?, #pending?, #pilot?, #released?, #skipped?

Methods inherited from Core::BaseClasses::ServerObject

#to_json

Methods included from Core::JSONWrappers

extended, included, #parse_json

Constructor Details

#initialize(data_hash) ⇒ Version

NOTE: be sure to only instantiate these using the servers ‘instantiate_version’ method, or else they might not have all the correct innards



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/xolo/server/version.rb', line 384

def initialize(data_hash)
  super

  # These attrs aren't defined in the ATTRIBUTES
  # and/or are not stored in the on-disk json file

  @ted_id_number ||= data_hash[:ted_id_number]
  @jamf_pkg_id ||= data_hash[:jamf_pkg_id]

  # and these can be generated now
  @jamf_obj_name_pfx = "#{jamf_obj_name_pfx_base}#{title}-#{version}"

  @jamf_pkg_name ||= @jamf_obj_name_pfx

  @jamf_installed_group_name = "#{jamf_obj_name_pfx}-#{JAMF_SMART_GROUP_NAME_INSTALLED_SFX}"

  @jamf_auto_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_INSTALL_SFX}"
  @jamf_manual_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_MANUAL_INSTALL_SFX}"
  @jamf_auto_reinstall_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_REINSTALL_SFX}"

  @jamf_patch_policy_name = @jamf_obj_name_pfx

  # we set @jamf_pkg_file when a pkg is uploaded
  # since we don't know until then if its a .pkg or .zip
  # It will be stored in the local data and reloaded as needed
end

Instance Attribute Details

#changes_for_updateHash (readonly)

Also when applying updates, this will hold the changes being made: the differences between tne current attributs and the new_data_for_update We’ll figure this out at the start of the update and can use it later to 1) avoid doing things we don’t need to 2) log the changes in the change log at the very end

This is a Hash with keys of the attribute names that have changed the values are Hashes with keys of :old and :new e.g. { pilot_groups: { old: [‘foo’], new: [‘bar’] } }

Returns:

  • (Hash)


368
369
370
# File 'lib/xolo/server/version.rb', line 368

def changes_for_update
  @changes_for_update
end

#current_actionSymbol

Returns The current action being taken on this title one of :creating, :updating, :deleting.

Returns:

  • (Symbol)

    The current action being taken on this title one of :creating, :updating, :deleting



372
373
374
# File 'lib/xolo/server/version.rb', line 372

def current_action
  @current_action
end

#jamf_auto_install_policy_nameObject (readonly)

Jamf auto-install policies are named this



333
334
335
# File 'lib/xolo/server/version.rb', line 333

def jamf_auto_install_policy_name
  @jamf_auto_install_policy_name
end

#jamf_auto_reinstall_policy_nameObject (readonly)

Jamf auto re-install policies are named this



339
340
341
# File 'lib/xolo/server/version.rb', line 339

def jamf_auto_reinstall_policy_name
  @jamf_auto_reinstall_policy_name
end

#jamf_installed_group_nameString (readonly)

For each version there will be a smart group containing all macs that have that version of the title installed. The smart group will be named ‘xolo-<title>-<version>-installed’

It will be used as the target for the auto-reinstall

Returns:

  • (String)

    the name of the smart group



330
331
332
# File 'lib/xolo/server/version.rb', line 330

def jamf_installed_group_name
  @jamf_installed_group_name
end

#jamf_manual_install_policy_nameObject (readonly) Also known as: jamf_manual_install_trigger

Jamf manual install policies are named this



336
337
338
# File 'lib/xolo/server/version.rb', line 336

def jamf_manual_install_policy_name
  @jamf_manual_install_policy_name
end

#jamf_obj_name_pfxObject (readonly)

Jamf object names start with this



321
322
323
# File 'lib/xolo/server/version.rb', line 321

def jamf_obj_name_pfx
  @jamf_obj_name_pfx
end

#jamf_patch_policy_nameObject (readonly)

Jamf Patch Policy is named this



345
346
347
# File 'lib/xolo/server/version.rb', line 345

def jamf_patch_policy_name
  @jamf_patch_policy_name
end

#jamf_pkg_idObject (readonly)

The Jamf Package object has this jamf id



348
349
350
# File 'lib/xolo/server/version.rb', line 348

def jamf_pkg_id
  @jamf_pkg_id
end

#new_data_for_updateObject (readonly)

when applying updates, the new data is stored here so it can be accessed by update-methods and compared to the current instanace values both for updating the title, and the versions



354
355
356
# File 'lib/xolo/server/version.rb', line 354

def new_data_for_update
  @new_data_for_update
end

#pkg_is_from_autopkgBoolean

Returns is the pkg being processed now from an autopkg recipe?.

Returns:

  • (Boolean)

    is the pkg being processed now from an autopkg recipe?



375
376
377
# File 'lib/xolo/server/version.rb', line 375

def pkg_is_from_autopkg
  @pkg_is_from_autopkg
end

#server_app_instanceObject

The instance of Xolo::Server::App that instantiated this version object. This is how we access things that are available in routes and helpers, like the single Jamf and TEd connections for this App instance.



308
309
310
# File 'lib/xolo/server/version.rb', line 308

def server_app_instance
  @server_app_instance
end

#ted_id_numberObject

The Windoo::Patch#patchId



318
319
320
# File 'lib/xolo/server/version.rb', line 318

def ted_id_number
  @ted_id_number
end

#title_object(refresh: false) ⇒ Xolo::Server::Title

This might have been set already if we were instantiated via our title

Returns:



571
572
573
574
# File 'lib/xolo/server/version.rb', line 571

def title_object(refresh: false)
  @title_object = nil if refresh
  @title_object ||= server_app_instance.instantiate_title title
end

Class Method Details

.add_version_via_subscription(title_object:, new_version:) ⇒ void

This method returns an undefined value.

add a new version in response to a patch title update webhook event. This doesn’t upload a pkg - it just creates the version in Xolo, and then someone can upload a pkg to it via xadm or autopkg will do it if configured.

Parameters:

  • title_object (Xolo::Server::Title)

    the title object for the subscribed title

  • new_version (String)

    the new version to add



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/xolo/server/version.rb', line 234

def self.add_version_via_subscription(title_object:, new_version:)
  title_object.log_info "Adding new version '#{new_version}' for subscribed title '#{title_object.title}'"

  # get more details about this version from the JPAPI
  patch_version_data = title_object.patch_versions(version: new_version).first
  unless patch_version_data
    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."
    title_object.log_error msg, alert: true
    return
  end

  title_object.log_debug "Got patch version data from JPAPI for version '#{new_version}': #{patch_version_data}"

  # put the data into a hash for creating a new version object
  vobj_data = {
    publish_date: Time.parse(patch_version_data[:releaseDate]),
    standalone: patch_version_data[:standalone],
    min_os: patch_version_data[:minimumOperatingSystem],
    reboot: patch_version_data[:rebootRequired],
    killapps: []
  }

  # Killapps for subscribed titles? The API only shows app names without the .app, e.g.
  # "killApps": [
  #   {
  #     "appName": "ChrislTestHelper"
  #   },
  #   {
  #     "appName": "Chrisl Test"
  #   }
  # ]
  # Since we don't manage them, we'll just record them in the data like this...
  unless patch_version_data[:killApps].pix_empty?
    patch_version_data[:killApps].each do |ka|
      vobj_data[:killapps] << "#{ka[:appName]}.app;unknown.from.subscription"
    end
  end

  # instantiate the version object
  title_object.log_debug "Instantiating version via subscription '#{new_version}' of title '#{title_object.title}' (#{title_object.class}) with data: #{vobj_data}"

  vobj = title_object.server_app_instance.instantiate_version(
    title: title_object,
    version: new_version,
    **vobj_data
  )

  # create it in xolo
  vobj.create

  # tell someone
  msg = "ACTION REQUIRED: New pilot version '#{new_version}' for subscribed title '#{title_object.title}' has been created in Xolo via subscription."

  # if not autopkg enabled, we need to tell someone to upload a pkg for this new version
  unless title_object.autopkg_enabled?
    # update general alert msg
    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"

    # email to title contact
    vobj.server_app_instance.send_email to: title_object.contact_email, subject: 'Need manual upload of xolo pkg', msg: msg
  end # if title_object.autopkg_enabled?

  # send general alert
  vobj.log_info msg, alert: true
end

.all_versions(title) ⇒ Array<String>

Returns A list of all known versions for a title, just the basenames of all the version files with the extension removed.

Returns:

  • (Array<String>)

    A list of all known versions for a title, just the basenames of all the version files with the extension removed



117
118
119
# File 'lib/xolo/server/version.rb', line 117

def self.all_versions(title)
  version_dirs(title).map { |c| c.basename.to_s }
end

.data_dir(title, version) ⇒ Pathname

The the local directory containing various files specific to the given version of a title

Returns:

  • (Pathname)


130
131
132
# File 'lib/xolo/server/version.rb', line 130

def self.data_dir(title, version)
  version_dir(title) + version
end

.data_file(title, version) ⇒ Pathname

The the local JSON file containing the current values for the given version of a title

Returns:

  • (Pathname)


143
144
145
# File 'lib/xolo/server/version.rb', line 143

def self.data_file(title, version)
  data_dir(title, version) + "#{version}.json"
end

.in_ted?(patch_id, cnx:) ⇒ Boolean

Returns Does the given patch exist in the Title Editor?.

Parameters:

  • patch_id (String)

    the id number of the patch we are looking for

Returns:

  • (Boolean)

    Does the given patch exist in the Title Editor?



182
183
184
# File 'lib/xolo/server/version.rb', line 182

def self.in_ted?(patch_id, cnx:)
  Windoo::Patch.all_ids(cnx: cnx).include? patch_id
end

.load(title, version) ⇒ Xolo::Server::Title

Instantiate from the local JSON file containing the current values for the given version of a title

NOTE: All instantiation should happen using the #instantiate_version method in the server app instance. Please don’t call this method directly

Returns:



173
174
175
176
# File 'lib/xolo/server/version.rb', line 173

def self.load(title, version)
  Xolo::Server.logger.debug "Loading version '#{version}' of title '#{title}' from file"
  new parse_json(data_file(title, version).read)
end

.locked?(title, version) ⇒ Boolean

Is a version locked for updates?

Returns:

  • (Boolean)


188
189
190
191
# File 'lib/xolo/server/version.rb', line 188

def self.locked?(title, version)
  curr_lock = Xolo::Server.object_locks.dig title, :versions, version
  curr_lock && curr_lock > Time.now
end

.manifest_file(title, version) ⇒ Pathname

The the local xml plist file containing the .pkg manifest for the given version of a title

Returns:

  • (Pathname)


156
157
158
# File 'lib/xolo/server/version.rb', line 156

def self.manifest_file(title, version)
  data_dir(title, version) + "#{version}.manifest.plist"
end

.pkg_deletion_poolQueue

The package-deletion thread pool

the auto_terminate is false to prevents the threads from being daemonized, and running after the main thread exits. This is important because launchd jobs should never do that.

See ruby-concurrency.github.io/concurrent-ruby/master/file.thread_pools.html

Returns:

  • (Queue)

    The package-deletion thread pool



202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/xolo/server/version.rb', line 202

def self.pkg_deletion_pool
  @pkg_deletion_pool ||= Concurrent::ThreadPoolExecutor.new(
    name: 'package-deletion',
    min_threads: 1, # start with 1 thread
    max_threads: MAX_PKG_DELETION_THREADS, # create at most 10 threads
    max_queue: 0, # no limit
    auto_terminate: false, # see method comments above
    idletime: 60 # seconds thread can remain idle before it is reclaimed, default is 60
    # fallback_policy: :abort # the default is :abort, which will raise a
    #   Concurrent::RejectedExecutionError exception and discard the task
  )
end

.pkg_deletion_pool_infoHash

info about the current pkg deletion pool state, for the /state route

Returns:

  • (Hash)


219
220
221
222
223
224
# File 'lib/xolo/server/version.rb', line 219

def self.pkg_deletion_pool_info
  {
    threads: pkg_deletion_pool.length,
    queued_tasks: pkg_deletion_pool.queue_length
  }
end

.version_dir(title) ⇒ Pathname

Returns The directory containing subdirectories for each version of a title. They contain JSON and other files for the versions.

Returns:

  • (Pathname)

    The directory containing subdirectories for each version of a title. They contain JSON and other files for the versions.



101
102
103
# File 'lib/xolo/server/version.rb', line 101

def self.version_dir(title)
  Xolo::Server::Title.title_dir(title) + VERSIONS_DIRNAME
end

.version_dirs(title) ⇒ Array<Pathname>

Returns All version directories for a title.

Returns:

  • (Array<Pathname>)

    All version directories for a title



108
109
110
111
# File 'lib/xolo/server/version.rb', line 108

def self.version_dirs(title)
  vdir = version_dir(title)
  vdir.directory? ? vdir.children : []
end

Instance Method Details

#<=>(other) ⇒ Object

version comparison

Raises:

See Also:

  • Comparable


418
419
420
421
422
423
# File 'lib/xolo/server/version.rb', line 418

def <=>(other)
  raise Xolo::InvalidDataError, 'Cannot compare with other classes' unless other.is_a? Xolo::Server::Version
  raise Xolo::InvalidDataError, 'Cannot compare versions of different titles' unless other.title == title

  order_index <=> other.order_index
end

#adminString

This can be manually set earlier in the request handling to use a non-standard admin username

Returns:

  • (String)


550
551
552
# File 'lib/xolo/server/version.rb', line 550

def admin
  @admin ||= session[:admin]
end

#createvoid

This method returns an undefined value.

Save a new version, adding to the local filesystem, Jamf Pro, and the Title Editor as needed This should be running in the context of #with_streaming



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
# File 'lib/xolo/server/version.rb', line 637

def create
  lock
  @current_action = :creating

  self.creation_date = Time.now
  self.created_by = admin
  self.status = STATUS_PENDING
  log_debug "creation_date: #{creation_date}, created_by: #{created_by}"

  # save to file here so that we have something to delete if
  # the next couple steps fail
  progress 'Saving version data to Xolo server'
  save_local_data

  create_patch_in_ted unless subscribed?

  create_in_jamf

  self.status = STATUS_PILOT

  # save to file again now, because saving to TitleEd and Jamf will
  # add some data
  save_local_data

  # prepend our version to the version_order array of the title
  progress "Updating title version_order, prepending '#{version}'", log: :info
  title_object.prepend_version(version)

  log_change msg: 'Version Created'

  progress "Version '#{version}' of Title '#{title}' has been created in Xolo.", log: :info

  # all done unless we need to get a pkg via autopkg
  # pkg upload from xadm will happen in a separate process,
  # so we don't want to do it here in the create method

  # do we have an uploaded pkg?
  if pkg_to_upload.to_s.start_with? '/'
    progress "Pkg will be uploaded to xolo via xadm shortly, from path '#{pkg_to_upload}'", log: :info

  # if we have an autopkg recipe and dir, get the .pkg and upload it to Jamf
  elsif title_object.autopkg_enabled?
    handle_autopkg_during_create

  # otherwise tell someone we need a .pkg
  else
    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."

    # no alert when subscribed because a better alert mesg is sent from add_version_via_subscription
    subscribed? ? progress(msg, log: :warn) : progress(msg, log: :warn, alert: true)
  end
ensure
  unlock
end

#creating?Boolean

Returns Are we creating this version?.

Returns:

  • (Boolean)

    Are we creating this version?



435
436
437
# File 'lib/xolo/server/version.rb', line 435

def creating?
  current_action == :creating
end

#data_dirPathname

The data directory for this version

Returns:

  • (Pathname)


593
594
595
# File 'lib/xolo/server/version.rb', line 593

def data_dir
  self.class.data_dir title, version
end

#data_filePathname

The JSON data file for this version

Returns:

  • (Pathname)


600
601
602
# File 'lib/xolo/server/version.rb', line 600

def data_file
  self.class.data_file title, version
end

#delete(update_title: true, deleting_title: false) ⇒ void

This method returns an undefined value.

Delete the version

Parameters:

  • update_title (Boolean) (defaults to: true)

    Update the title for this version to know the version is gone. Set this to false when the title itself is being deleted and calling this method.

  • deleting_title (Boolean) (defaults to: false)

    Is the title itself being deleted?



980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
# File 'lib/xolo/server/version.rb', line 980

def delete(update_title: true, deleting_title: false)
  lock
  @current_action = :deleting

  delete_version_from_jamf

  # NOTE: we no longer delete the patch from the Title Editor
  # unless the whole title is being deleted, because
  # patches may be needed for reporting purposes.
  # When the title is deleted, the title's delete method
  # will delete all patches for all versions.
  # If other situations arise where we need to delete
  # ted patches individually, set deleting_title to true.
  delete_patch_from_ted if deleting_title && managed?

  # remove from the title's list of versions
  progress 'Deleting version from title data on the Xolo server', log: :debug
  title_object.remove_version(version) if update_title

  # delete the local data
  progress 'Deleting version data from the Xolo server', log: :info
  data_dir.rmtree
  log_change msg: 'Version Deleted'

  progress "Version '#{version}' of Title '#{title}' has been deleted from Xolo.", log: :info
ensure
  unlock
end

#deleting?Boolean

Returns Are we deleting this version?.

Returns:

  • (Boolean)

    Are we deleting this version?



453
454
455
# File 'lib/xolo/server/version.rb', line 453

def deleting?
  current_action == :deleting
end

#deprecatevoid

This method returns an undefined value.

deprecate this version



874
875
876
877
878
879
880
881
882
883
884
885
886
# File 'lib/xolo/server/version.rb', line 874

def deprecate
  lock
  progress "Deprecating older released version '#{version}'"
  disable_policies_for_deprecation_or_skipping :deprecated
  self.status = STATUS_DEPRECATED
  self.deprecation_date = Time.now
  self.deprecated_by = admin
  log_change msg: 'Version Deprecated'

  save_local_data
ensure
  unlock
end

#excluded_groups_to_use(ttl_obj: nil) ⇒ Array<String>

The scope excluded groups to use in policies and patch policies for all versions of this title.

Excluded groups are defined in the title, applying to all versions, and may be augmented by:

  • Xolo::Server.config.forced_exclusion, a group excluded from ALL of xolo, defined in the server config.

  • The title’s jamf_frozen_group_name, if it exists, containing computers that have been ‘frozen’ to a single version.

For initial install policies, the smart group of macs with any version installed (jamf_installed_group_name) “xolo-<title>-installed” is also excluded, because otherwise the initial-install policies would stomp on the patch policies.

Parameters:

  • ttl_obj (Xolo::Server::Title) (defaults to: nil)

    The pre-instantiated title for ths version. if nil, we’ll instantiate it now

Returns:

  • (Array<String>)

    the excluded groups to use



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/xolo/server/version.rb', line 502

def excluded_groups_to_use(ttl_obj: nil)
  return @excluded_groups_to_use if @excluded_groups_to_use

  ttl_obj ||= title_object
  # get the excluded groups from the title
  # Use .dup so we don't modify the original
  @excluded_groups_to_use = ttl_obj.changes_for_update&.key?(:excluded_groups) ? ttl_obj.changes_for_update[:excluded_groups][:new].dup : ttl_obj.excluded_groups.dup

  # always exclude the frozen static group
  # calling ttl_obj.jamf_frozen_group will create the group if needed
  @excluded_groups_to_use << ttl_obj.jamf_frozen_group.name
  log_debug "Appended '#{ttl_obj.jamf_frozen_group_name}' to @excluded_groups_to_use"

  # always exclude Xolo::Server.config.forced_exclusion if defined
  @excluded_groups_to_use << valid_forced_exclusion_group_name if valid_forced_exclusion_group_name

  @excluded_groups_to_use.uniq!
  log_debug "Excluded groups to use: #{@excluded_groups_to_use.join ', '}"

  @excluded_groups_to_use
end

#handle_autopkg_during_createObject

Do autopkg stuff during creation



695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# File 'lib/xolo/server/version.rb', line 695

def handle_autopkg_during_create
  return unless title_object.autopkg_enabled?

  pkg_src = title_object.run_autopkg_recipe

  if pkg_src.nil?
    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.'
    progress msg, log: :warn, alert: true
    return
  end

  oldest_allowed = Time.now - 1200 # 20 minutes ago
  if pkg_src.mtime < oldest_allowed
    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."
    progress msg, log: :warn, alert: true
    return
  end

  # this lets future code know that the pkg we're working with came from autopkg
  self.pkg_is_from_autopkg = true

  # Upload the pkg to Jamf, and associate it with this version
  server_app_instance.process_and_upload_autopkg_pkg(title, self, pkg_src)
end

#jamf_cnx(refresh: false) ⇒ Jamf::Connection

Returns a single Jamf Pro API connection to use for the life of this instance.

Returns:

  • (Jamf::Connection)

    a single Jamf Pro API connection to use for the life of this instance



586
587
588
# File 'lib/xolo/server/version.rb', line 586

def jamf_cnx(refresh: false)
  server_app_instance.jamf_cnx refresh: refresh
end

#lockObject

Lock this version for updates

Raises:



1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
# File 'lib/xolo/server/version.rb', line 1017

def lock
  raise Xolo::ServerError, 'Server is shutting down' if Xolo::Server.shutting_down?

  while locked?
    if (Time.now.to_i % 5).zero?
      log_debug "Method #{caller_locations.first.label} is waiting for update lock on Version '#{version}' of title '#{title}'..."
    end
    sleep 0.33
  end
  Xolo::Server.object_locks[title] ||= { versions: {} }

  exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
  Xolo::Server.object_locks[title][:versions][version] = exp
  log_debug "Locked version '#{version}' of title '#{title}' for updates until #{exp}, by method #{caller_locations.first.label}"
end

#locked?Boolean

Is this version locked for updates?

Returns:

  • (Boolean)


1011
1012
1013
# File 'lib/xolo/server/version.rb', line 1011

def locked?
  self.class.locked?(title, version)
end

#managed?Boolean

or a managed title?

Returns:

  • (Boolean)


726
727
728
# File 'lib/xolo/server/version.rb', line 726

def managed?
  !subscribed?
end

#manifest_filePathname

The manifest plist file for this version

Returns:

  • (Pathname)


607
608
609
# File 'lib/xolo/server/version.rb', line 607

def manifest_file
  self.class.manifest_file title, version
end

#order_indexInteger

Returns The index of this version in the title’s reversed version_order array. We reverse it because the version_order array holds the newest versions first, so the index of the newest version is 0, the next newest is 1, etc - we need the opposite of that.

Returns:

  • (Integer)

    The index of this version in the title’s reversed version_order array. We reverse it because the version_order array holds the newest versions first, so the index of the newest version is 0, the next newest is 1, etc - we need the opposite of that.



429
430
431
# File 'lib/xolo/server/version.rb', line 429

def order_index
  title_object.version_order.reverse.index version
end

#pilot_groups_to_useArray<String>

The scope target groups to use in policies and patch policies during pilot This is defined in each version, and inherited when new versions are created.

Returns:

  • (Array<String>)

    the pilot groups to use



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/xolo/server/version.rb', line 468

def pilot_groups_to_use
  return @pilot_groups_to_use if @pilot_groups_to_use

  # any defined in the version override any in the title
  @pilot_groups_to_use = changes_for_update&.key?(:pilot_groups) ? changes_for_update[:pilot_groups][:new] : pilot_groups
  return @pilot_groups_to_use unless @pilot_groups_to_use.empty?

  # if none defined in the version, look in the title
  @pilot_groups_to_use =
    if title_object.changes_for_update&.key?(:pilot_groups)
      title_object.changes_for_update[:pilot_groups][:new]
    else
      title_object.pilot_groups
    end
end

#progress(msg, log: :debug, alert: false) ⇒ void

This method returns an undefined value.

Append a message to the progress stream file, optionally sending it also to the log

Parameters:

  • message (String)

    the message to append

  • log (Symbol) (defaults to: :debug)

    the level at which to log the message one of :debug, :info, :warn, :error, :fatal, or :unknown. Default is nil, which doesn’t log the message at all.



564
565
566
# File 'lib/xolo/server/version.rb', line 564

def progress(msg, log: :debug, alert: false)
  server_app_instance.progress msg, log: log, alert: alert
end

#release(rollback:) ⇒ void

This method returns an undefined value.

Release this version, possibly rolling back from a previously newer version This should only be called by the title. The initial ‘release’ action starts in the title, and then calls this method on the version to do the version-specific release steps.

Parameters:

  • rollback (Boolean)

    If true, this version is being released as a rollback



831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# File 'lib/xolo/server/version.rb', line 831

def release(rollback:)
  lock
  @current_action = :releasing
  # set scope targets of auto-install policy to release-groups
  msg = "Jamf: Version '#{version}': Setting scope targets of auto-install policy to release_groups: #{release_groups_to_use.join(', ')}"
  progress msg, log: :info
  pol = jamf_auto_install_policy
  set_policy_release_groups pol
  pol.enable
  pol.save

  # set scope targets of patch policy to all (in patch pols, 'all' means 'all eligible')
  msg = "Jamf: Version '#{version}': Setting scope targets of patch policy to all eligible computers"
  progress msg, log: :info
  ppol = jamf_patch_policy
  ppol.scope.set_all_targets

  # if rollback, make sure the patch policy is set to 'allow downgrade'
  if rollback
    msg = "Jamf: Version '#{version}': Setting patch policy to allow downgrade"
    progress msg, log: :info
    ppol.allow_downgrade = true
  else
    ppol.allow_downgrade = false
  end
  ppol.save

  # change status to 'released'
  self.status = STATUS_RELEASED
  self.release_date = Time.now
  self.released_by = admin
  chg_msg = rollback ? 'Version Released - Rolled Back' : 'Version Released'
  log_change msg: chg_msg

  save_local_data
ensure
  unlock
end

#release_groups_to_use(ttl_obj: nil) ⇒ Array<String>

The scope target groups to use in policies and patch policies when the version is released This is defined in the title and applies to all versions.

Parameters:

  • ttl_obj (Xolo::Server::Title) (defaults to: nil)

    The pre-instantiated title for ths version. if nil, we’ll instantiate it now

Returns:

  • (Array<String>)

    the target groups to use



532
533
534
535
536
537
# File 'lib/xolo/server/version.rb', line 532

def release_groups_to_use(ttl_obj: nil)
  return @release_groups_to_use if @release_groups_to_use

  ttl_obj ||= title_object
  @release_groups_to_use = ttl_obj.changes_for_update&.key?(:release_groups) ? ttl_obj.changes_for_update[:release_groups][:new] : ttl_obj.release_groups
end

#releasing?Boolean

Returns Are we releasing this version?.

Returns:

  • (Boolean)

    Are we releasing this version?



459
460
461
# File 'lib/xolo/server/version.rb', line 459

def releasing?
  current_action == :releasing
end

#repairObject

Repair this version. Look at the Title Editor patch object, and ensure it’s correct based on the local data file.

- version order
- min os
- max os
- standalone
- reboot
- release date
- killapps
- component criteria
  - component name '<title>'
- capability criteria
- enabled

Then look at the various Jamf objects pertaining to this version, and ensure they are correct

- package object 'xolo-<title>-<version>'
  - filename 'xolo-<title>-<version>.pkg'
  - description
  - os limitations
- auto install policy 'xolo-<title>-<version>-auto-install'
- manual install policy  'xolo-<title>-<version>-manual-install'
- patch policy 'xolo-<title>-<version>'

Then look at the xolo metadata, and fix whatever is needed



795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
# File 'lib/xolo/server/version.rb', line 795

def repair
  lock
  @current_action = :repairing
  log_change msg: "Repairing version '#{version}'"
  progress "Starting repair of version '#{version}' of title '#{title}'", log: :debug

  repair_ted_patch
  repair_jamf_version_objects

  # If there's a reupload, but no original, make the orig the same as the re
  unless upload_date
    if reupload_date && reuploaded_by
      new_date = reupload_date
      new_by = reuploaded_by
    else
      new_date = Time.parse '2025-02-15'
      new_by = 'buzzlightyear'
    end
    progress "Fixing original upload date: #{new_date}, by: #{new_by}", log: :debug
    self.upload_date = new_date
    self.uploaded_by = new_by

  end
  save_local_data
ensure
  unlock
end

#repairing?Boolean

Returns Are we repairing this version?.

Returns:

  • (Boolean)

    Are we repairing this version?



447
448
449
# File 'lib/xolo/server/version.rb', line 447

def repairing?
  current_action == :repairing
end

#reset_to_pilotvoid

This method returns an undefined value.

Reset this version to ‘pilot’ status, since we are rolling back to a previous version



910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
# File 'lib/xolo/server/version.rb', line 910

def reset_to_pilot
  return if status == STATUS_PILOT

  lock
  progress "Resetting version '#{version}' to pilot status due to rollback of an older version"
  reset_policies_to_pilot
  self.status = STATUS_PILOT
  self.skipped_date = nil
  self.skipped_by = nil
  self.deprecation_date = nil
  self.deprecated_by = nil
  log_change msg: 'Version Reset to Pilot'
  save_local_data
ensure
  unlock
end

#save_local_datavoid

This method returns an undefined value.

Save our current data out to our JSON data file This overwrites the existing data.



958
959
960
961
962
963
964
965
966
967
968
# File 'lib/xolo/server/version.rb', line 958

def save_local_data
  data_dir.mkpath

  self.modification_date = Time.now
  self.modified_by = admin
  log_debug "Version '#{version}' of Title '#{title}' noting modification by #{modified_by}"

  file = data_file
  log_debug "Saving local version data to: #{file}"
  file.pix_atomic_write to_json
end

#sessionHash

Returns:

  • (Hash)


541
542
543
544
# File 'lib/xolo/server/version.rb', line 541

def session
  server_app_instance&.session || {}
  # @session ||= {}
end

#skipvoid

This method returns an undefined value.

skip this version



892
893
894
895
896
897
898
899
900
901
902
903
# File 'lib/xolo/server/version.rb', line 892

def skip
  lock
  progress "Skipping unreleased version '#{version}'"
  disable_policies_for_deprecation_or_skipping :skipped
  self.status = STATUS_SKIPPED
  self.skipped_date = Time.now
  self.skipped_by = admin
  log_change msg: 'Version Skipped'
  save_local_data
ensure
  unlock
end

#subscribed?Boolean

Is this version part of a subscribed title?

Returns:

  • (Boolean)


721
722
723
# File 'lib/xolo/server/version.rb', line 721

def subscribed?
  title_object.subscribed?
end

#ted_cnxWindoo::Connection

Returns a single Title Editor connection to use for the life of this instance.

Returns:

  • (Windoo::Connection)

    a single Title Editor connection to use for the life of this instance



579
580
581
# File 'lib/xolo/server/version.rb', line 579

def ted_cnx
  server_app_instance.ted_cnx
end

#ted_patch(refresh: false) ⇒ Windoo::Patch

TODO: maybe pass in an appropriate Windoo::SoftwareTitle, so we don’t have to use refresh all the time to re-fetch, if we just re-fetched from elsewhere?

Returns:

  • (Windoo::Patch)

    The Windoo::Patch object that represents this version in the title editor



618
619
620
621
# File 'lib/xolo/server/version.rb', line 618

def ted_patch(refresh: false)
  @ted_patch = nil if refresh
  @ted_patch ||= ted_title(refresh: refresh).patches.patch(version)
end

#ted_title(refresh: false) ⇒ Windoo::SoftwareTitle

Returns The Windoo::SoftwareTitle object that represents this version’s title in the title editor.

Returns:

  • (Windoo::SoftwareTitle)

    The Windoo::SoftwareTitle object that represents this version’s title in the title editor



626
627
628
629
# File 'lib/xolo/server/version.rb', line 626

def ted_title(refresh: false)
  @ted_title = nil if refresh
  @ted_title ||= Windoo::SoftwareTitle.fetch id: title, cnx: ted_cnx
end

#to_hObject

Add more data to our hash



1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
# File 'lib/xolo/server/version.rb', line 1045

def to_h
  hash = super

  # These attrs aren't defined in the ATTRIBUTES
  # but we want them in the hash and/or JSON
  hash[:jamf_pkg_id] = jamf_pkg_id
  hash[:ted_id_number] = ted_id_number
  hash[:pilot_groups_to_use] = pilot_groups_to_use
  hash[:release_groups_to_use] = release_groups_to_use

  hash.sort.to_h
end

#unlockObject

Unlock this version for updates



1035
1036
1037
1038
1039
1040
1041
# File 'lib/xolo/server/version.rb', line 1035

def unlock
  curr_lock = Xolo::Server.object_locks.dig title, :versions, version
  return unless curr_lock

  Xolo::Server.object_locks[title][:versions].delete version
  log_debug "Unlocked version '#{version}' of title '#{title}' for updates"
end

#update(new_data) ⇒ void

This method returns an undefined value.

Update a this version, updating to the local filesystem, Jamf Pro, and the Title Editor as needed

Parameters:

  • new_data (Hash)

    The new data sent from xadm



736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
# File 'lib/xolo/server/version.rb', line 736

def update(new_data)
  lock
  @current_action = :updating
  @new_data_for_update = new_data
  @changes_for_update = note_changes_for_update_and_log
  if @changes_for_update.pix_empty?
    progress 'No changes to make', log: :info
    return
  end

  log_info "Updating version '#{version}' of title '#{title}' for admin '#{admin}'"

  # changelog - log the changes now, and
  # if there is an error, we'll log that too
  # saying the above changes were not completed and to
  # look at the server log for details.
  log_update_changes

  # update ted before jamf
  update_patch_in_ted
  update_version_in_jamf
  update_local_instance_values
  save_local_data

  # new pkg uploads happen in a separate process
rescue => e
  log_change msg: "ERROR: The update failed and the changes didn't all go through!\n#{e.class}: #{e.message}\nSee server log for details."

  # re-raise for proper error handling in the server app
  raise
ensure
  unlock
end

#update_local_instance_valuesvoid

This method returns an undefined value.

Update our instance attributes with any new data before saving the changes back out to the file system



931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/xolo/server/version.rb', line 931

def update_local_instance_values
  # update instance data with new data before writing out to the filesystem.
  # Do this last so that the instance values can be compared to
  # new_data_for_update in the steps above.
  # Also, those steps might have updated some server-specific attributes
  # which will be saved to the file system as well.
  ATTRIBUTES.each do |attr, deets|
    # make sure these are updated elsewhere if needed,
    # e.g. modification data.
    next if deets[:read_only]
    next unless deets[:cli]

    new_val = new_data_for_update[attr]
    old_val = send(attr)
    next if new_val == old_val

    log_debug "Updating Xolo Version attribute '#{attr}': '#{old_val}' -> '#{new_val}'"
    send "#{attr}=", new_val
  end
  # update any other server-specific attributes here...
end

#updating?Boolean

Returns Are we updating this version?.

Returns:

  • (Boolean)

    Are we updating this version?



441
442
443
# File 'lib/xolo/server/version.rb', line 441

def updating?
  current_action == :updating
end