Class: Xolo::Server::Title

Inherits:
Core::BaseClasses::Title
  • Object
show all
Includes:
Helpers::JamfPro, Helpers::Log, Helpers::Notification, Helpers::TitleEditor, Mixins::Changelog, Mixins::TitleJamfAccess, Mixins::TitleTedAccess
Defined in:
lib/xolo/server/title.rb

Overview

A Title in Xolo, as used on the server

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

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 servers ‘instantiate_title’ method, or else they might not have all the correct innards

Constant Summary collapse

TITLES_DIR =

On the server, xolo titles are represented by directories in this directory, named with the title name.

So a title ‘foobar’ would have a directory

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

and in there will be a file

foobar.json

with the data for the Title instance itself

Also in there will be a ‘versions’ dir containing json files for each version of the title. See Version

Xolo::Server::DATA_DIR + 'titles'
NEW_TITLE_CURRENT_VERSION =

when creating new titles in the title editor, This is the ‘currentVersion’, which is required when creating. When the first version/patch is added, the title’s value for this will be updated.

'0.0.0'
VERSION_SCRIPT_FILENAME =

If a title has a ‘version_script’ the the contents are stored in the title dir in a file with this name

'version-script'
UNINSTALL_SCRIPT_FILENAME =

If a title is uninstallable, it will have a script in Jamf, which is also saved in this file on the xolo server.

'uninstall-script'
TITLE_EDITOR_EA_KEY_PREFIX =

In the TitleEditor, the version script is stored as an Extension Attribute - each title can only have one. and it needs a ‘key’, which is the name used to indicate the EA in various criteria, and is the EA name in Jamf Patch. The key is this value as a prefix on the title so for title ‘foobar’, it is ‘xolo-foobar’ That value is also used as the display name

Xolo::Server::JAMF_OBJECT_NAME_PFX
JAMF_NORMAL_EA_NAME_SUFFIX =

The EA from the title editor, which is used in Jamf Patch cannot, unfortunately, be used as a criterion in normal smart groups or advanced searches. Since we need a smart group containing all macs with any version of the title installed, we need a second copy of the EA as a ‘normal’ EA.

(That group is used as an exclusion to any auto-install initial- install policies, so that those policies don’t stomp on the matching Patch Policies)

The ‘duplicate’ EA is named the same as the Titled Editor key (see TITLE_EDITOR_EA_KEY_PREFIX) with this suffix added. So for the Title Editor key ‘xolo-<title>’, we’ll also have a matching normal EA called ‘xolo-<title>-installed-version’

'-installed-version'
JAMF_INSTALLED_GROUP_NAME_SUFFIX =
'-installed'
JAMF_FROZEN_GROUP_NAME_SUFFIX =
'-frozen'
JAMF_UNINSTALL_SUFFIX =
'-uninstall'
JAMF_EXPIRE_SUFFIX =
'-expire'
CLIENT_EXPIRE_COMMAND =

the expire policy will run this client command, appending the title We don’t specify a full path so that localized installations will work as long as its in roots default path e.g. /usr/local/bin vs /usr/local/pixar/bin

'xolo expire'
SELF_SERVICE_ICON_FILENAME =

When we are given a Self Service icon for the title, we might not be ready to upload it to jamf, cuz until we have a version to pilot, there’s nothing IN jamf. So we always store it locally in this file inside the title dir. The extension from the original file will be appended, e.g. ‘.png’

'self-service-icon'
JPAPI_PATCH_TITLE_RSRC =

The JPAPI endpoint for Patch Titles.

ruby-jss still uses the Classic API for Patch Titles, and won’t by migrated to JPAPI until Jamf fully implements all aspects of patch management to JPAPI. As of this writing, that’s not the case. But, the JPAPI endpoint for Patch Title Reporting returns more detailed data than the Classic API, so we use it here, and will keep using it as we move forward.

This is the top-level endpoint for all patch titles, see JPAPI_PATCH_REPORT_RSRC for the reporting endpoint below it.

TODO: Remove this and update relevant methods when ruby-jss is updated to use JPAPI for Patch Titles..

'v2/patch-software-title-configurations'
JPAPI_PATCH_REPORT_RSRC =

The JPAPI endpoint for patch reporting. The JPAPI_PATCH_TITLE_RSRC is appended with “/<id>/#JPAPI_PATCH_REPORT_RSRC” to get the URL for the patch report for a specific title.

TODO: Remove this and update relevant methods when ruby-jss is updated to use JPAPI for Patch Titles..

'patch-report'
SELF_SERVICE_INSTALL_BTN_TEXT =
'Install'

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mixins::TitleTedAccess

#any_ted_changes?, #apply_requirement_changes, #create_and_enable_stub_patch_in_ted, #create_ted_ea, #create_ted_title_requirements, #create_title_in_ted, #delete_ted_ea, #delete_title_from_ted, #enable_ted_title, included, #repair_ted_title, #requirement_change, #set_app_requirement, #set_ea_requirement, #set_ted_patch_component_criteria_after_update, #set_ted_title_requirement, #ted_ea_key, #ted_title, #ted_title_url, #update_ted_ea, #update_title_in_ted

Methods included from Mixins::TitleJamfAccess

#accept_jamf_patch_ea, #accept_jamf_patch_ea_via_api, #activate_jamf_patch_title, #add_title_to_self_service, #auto_accept_patch_ea_in_thread, #configure_jamf_expire_policy, #configure_jamf_installed_group, #configure_jamf_manual_install_released_policy, #configure_jamf_normal_ea, #configure_jamf_uninstall_policy, #configure_jamf_uninstall_script, #configure_pol_for_self_service, #create_jamf_manual_install_released_policy, #create_title_in_jamf, #delete_jamf_expire_policy, #delete_jamf_frozen_group, #delete_jamf_installed_group, #delete_jamf_manual_install_released_policy, #delete_jamf_normal_ea, #delete_jamf_patch_title, #delete_jamf_uninstall_policy, #delete_jamf_uninstall_script, #delete_title_from_jamf, #freeze_computers, #freeze_or_thaw_computers, #frozen_computers, included, #jamf_active_ted_titles, #jamf_expire_policy, #jamf_expire_policy_exist?, #jamf_expire_policy_url, #jamf_frozen_group, #jamf_frozen_group_exist?, #jamf_frozen_group_url, #jamf_gui_url, #jamf_installed_group, #jamf_installed_group_criteria, #jamf_installed_group_exist?, #jamf_installed_group_url, #jamf_manual_install_released_policy, #jamf_manual_install_released_policy_exist?, #jamf_manual_install_released_policy_url, #jamf_normal_ea, #jamf_normal_ea_contents, #jamf_normal_ea_exist?, #jamf_normal_ea_url, #jamf_patch_ea_awaiting_acceptance?, #jamf_patch_ea_contents, #jamf_patch_ea_data, #jamf_patch_ea_matches_version_script?, #jamf_patch_ea_url, #jamf_patch_title, #jamf_patch_title_id, #jamf_patch_title_url, #jamf_ted_available_titles, #jamf_ted_patch_source, #jamf_ted_title_active?, #jamf_ted_title_available?, #jamf_uninstall_policy, #jamf_uninstall_policy_exist?, #jamf_uninstall_policy_url, #jamf_uninstall_script, #jamf_uninstall_script_exist?, #jamf_uninstall_script_url, #need_to_accept_jamf_patch_ea?, #need_to_delete_jamf_uninstall_script?, #need_to_update_description?, #need_to_update_expiration?, #need_to_update_jamf_installed_group?, #need_to_update_jamf_normal_ea?, #need_to_update_jamf_uninstall_script?, #patch_report, #repair_frozen_group, #repair_jamf_expire_policy, #repair_jamf_manual_install_released_policy, #repair_jamf_normal_ea, #repair_jamf_title_objects, #repair_jamf_uninstall_policy, #repair_jamf_uninstall_script, #thaw_computers, #update_description_in_jamf, #update_ssvc, #update_ssvc_category, #update_ssvc_icon, #update_title_in_jamf, #update_versions_for_title_changes_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_xolo_category_id, #valid_forced_exclusion_group_name

Constructor Details

#initialize(data_hash) ⇒ Title

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



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/xolo/server/title.rb', line 376

def initialize(data_hash)
  super

  @ted_id_number ||= data_hash[:ted_id_number]
  @jamf_patch_title_id ||= data_hash[:jamf_patch_title_id]
  @version_order ||= []
  @new_data_for_update = {}
  @changes_for_update = {}
  @jamf_installed_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_INSTALLED_GROUP_NAME_SUFFIX}"
  @jamf_frozen_group_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_FROZEN_GROUP_NAME_SUFFIX}"

  @jamf_manual_install_released_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}-install"

  @jamf_uninstall_script_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
  @jamf_uninstall_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_UNINSTALL_SUFFIX}"
  @jamf_expire_policy_name = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{data_hash[:title]}#{JAMF_EXPIRE_SUFFIX}"
end

Instance Attribute Details

#changes_for_updateHash (readonly)

Also when applying updates, this will hold the changes being made: the differences between tne current attributes 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. { release_groups: { old: [‘foo’], new: [‘bar’] } }

Returns:

  • (Hash)

    The changes being made



352
353
354
# File 'lib/xolo/server/title.rb', line 352

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



360
361
362
# File 'lib/xolo/server/title.rb', line 360

def current_action
  @current_action
end

#jamf_expire_policy_nameString (readonly)

If a title is expirable, it will have a policy in Jamf named ‘xolo-<title>-expire’ that will run the expiration process daily, at checkin or using a trigger of the same name.

Returns:

  • (String)

    the name of the policy to uninstall the title



319
320
321
# File 'lib/xolo/server/title.rb', line 319

def jamf_expire_policy_name
  @jamf_expire_policy_name
end

#jamf_frozen_group_nameString (readonly)

For each title there will be a static group containing macs that should not get any automatic installs or updates, They should be ‘frozen’ at whatever version was installed when they were added to the group. It will be named ‘xolo-<title>-frozen’

It will be used as an exclusion for the installation policies and the patch policy for each version.

Membership is maintained using ‘xadm freeze <title> <computer> [<computer> …]` and `xadm thaw <title> <computer> [<computer> …]`

Use ‘xadm report <title> frozen` to see a list.

If computer groups are used with freeze/thaw, they are expanded and their members added/removed individually in the static group

Since there is one such group per title, it’s name is stored here

Returns:

  • (String)

    the name of the smart group



294
295
296
# File 'lib/xolo/server/title.rb', line 294

def jamf_frozen_group_name
  @jamf_frozen_group_name
end

#jamf_installed_group_nameString (readonly)

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

It will be used as an exclusion for the initial auto-installation policy for each version since if the title is installed at all, any installation is not ‘initial’ but an update, and will be handled by the Patch Policy.

Since there is one such group per title, it’s name is stored here

Returns:

  • (String)

    the name of the smart group



273
274
275
# File 'lib/xolo/server/title.rb', line 273

def jamf_installed_group_name
  @jamf_installed_group_name
end

#jamf_manual_install_released_policy_nameObject (readonly)

The name of the policy that does initial manual or self-service installs of the currently-released version of this title. It will be named ‘xolo-<title>-install’



299
300
301
# File 'lib/xolo/server/title.rb', line 299

def jamf_manual_install_released_policy_name
  @jamf_manual_install_released_policy_name
end

#jamf_uninstall_policy_nameString (readonly)

If a title is uninstallable, it will have a policy in Jamf named ‘xolo-<title>-uninstall’ that will run the script of the same name, using a trigger of the same name.

Returns:

  • (String)

    the name of the policy to uninstall the title



312
313
314
# File 'lib/xolo/server/title.rb', line 312

def jamf_uninstall_policy_name
  @jamf_uninstall_policy_name
end

#jamf_uninstall_script_nameString (readonly)

If a title is uninstallable, it will have a script in Jamf named ‘xolo-<title>-uninstall’

Returns:

  • (String)

    the name of the script to uninstall the title



305
306
307
# File 'lib/xolo/server/title.rb', line 305

def jamf_uninstall_script_name
  @jamf_uninstall_script_name
end

#new_data_for_updateHash (readonly)

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

Returns:

  • (Hash)

    The new data to apply as an update



337
338
339
# File 'lib/xolo/server/title.rb', line 337

def new_data_for_update
  @new_data_for_update
end

#releasing_versionString

Returns If current action is :releasing, this is the version being released.

Returns:

  • (String)

    If current action is :releasing, this is the version being released



364
365
366
# File 'lib/xolo/server/title.rb', line 364

def releasing_version
  @releasing_version
end

#server_app_instanceXolo::Server::App

The instance of Xolo::Server::App that instantiated this title 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.

Returns:



326
327
328
# File 'lib/xolo/server/title.rb', line 326

def server_app_instance
  @server_app_instance
end

#ssvc_icon_idInteger

Returns The Jamf Pro ID for the self-service icon once it has been uploaded.

Returns:

  • (Integer)

    The Jamf Pro ID for the self-service icon once it has been uploaded



356
357
358
# File 'lib/xolo/server/title.rb', line 356

def ssvc_icon_id
  @ssvc_icon_id
end

#ted_id_numberInteger

Returns The Windoo::SoftwareTitle#softwareTitleId.

Returns:

  • (Integer)

    The Windoo::SoftwareTitle#softwareTitleId



329
330
331
# File 'lib/xolo/server/title.rb', line 329

def ted_id_number
  @ted_id_number
end

Class Method Details

.all_titlesArray<String>

Returns A list of all known titles, just the basenames of all the title_dirs.

Returns:

  • (Array<String>)

    A list of all known titles, just the basenames of all the title_dirs



163
164
165
# File 'lib/xolo/server/title.rb', line 163

def self.all_titles
  title_dirs.map(&:basename).map(&:to_s)
end

.in_ted?(title, cnx:) ⇒ Boolean

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

Parameters:

  • title (String)

    the title we are looking for

Returns:

  • (Boolean)

    Does the given title exist in the Title Editor?



246
247
248
# File 'lib/xolo/server/title.rb', line 246

def self.in_ted?(title, cnx:)
  Windoo::SoftwareTitle.all_ids(cnx: cnx).include? title
end

.jamf_normal_ea_name(title) ⇒ String

Returns The display name of a version script as a normal EA in Jamf, which can be used in Smart Groups and Adv Searches.

Returns:

  • (String)

    The display name of a version script as a normal EA in Jamf, which can be used in Smart Groups and Adv Searches.



177
178
179
# File 'lib/xolo/server/title.rb', line 177

def self.jamf_normal_ea_name(title)
  "#{ted_ea_key(title)}#{JAMF_NORMAL_EA_NAME_SUFFIX}"
end

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

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

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

Returns:



237
238
239
240
# File 'lib/xolo/server/title.rb', line 237

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

.locked?(title) ⇒ Boolean

Is a title locked for updates?

Returns:

  • (Boolean)


252
253
254
255
# File 'lib/xolo/server/title.rb', line 252

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

.ssvc_icon_file(title) ⇒ Pathname

Returns The the local file containing the self-service icon.

Returns:

  • (Pathname)

    The the local file containing the self-service icon



223
224
225
# File 'lib/xolo/server/title.rb', line 223

def self.ssvc_icon_file(title)
  title_dir(title).children.select { |c| c.basename.to_s.start_with? SELF_SERVICE_ICON_FILENAME }.first
end

.ted_ea_key(title) ⇒ String

Returns The key and display name of a version script stored in the title editor as the ExtAttr for a given title.

Returns:

  • (String)

    The key and display name of a version script stored in the title editor as the ExtAttr for a given title



170
171
172
# File 'lib/xolo/server/title.rb', line 170

def self.ted_ea_key(title)
  "#{TITLE_EDITOR_EA_KEY_PREFIX}#{title}"
end

.title_data_file(title) ⇒ Pathname

The the local JSON file containing the current values for the given title

Returns:

  • (Pathname)


199
200
201
# File 'lib/xolo/server/title.rb', line 199

def self.title_data_file(title)
  title_dir(title) + "#{title}.json"
end

.title_dir(title) ⇒ Pathname

The title dir for a given title on the server, which may or may not exist.

Returns:

  • (Pathname)


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

def self.title_dir(title)
  TITLES_DIR + title
end

.title_dirsArray<Pathname>

Returns A list of all known title dirs.

Returns:

  • (Array<Pathname>)

    A list of all known title dirs



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

def self.title_dirs
  TITLES_DIR.children
end

.uninstall_script_file(title) ⇒ Pathname

Returns The the local file containing the code of the version script.

Returns:

  • (Pathname)

    The the local file containing the code of the version script



215
216
217
# File 'lib/xolo/server/title.rb', line 215

def self.uninstall_script_file(title)
  title_dir(title) + UNINSTALL_SCRIPT_FILENAME
end

.version_script_file(title) ⇒ Pathname

Returns The the local file containing the code of the version script.

Returns:

  • (Pathname)

    The the local file containing the code of the version script



207
208
209
# File 'lib/xolo/server/title.rb', line 207

def self.version_script_file(title)
  title_dir(title) + VERSION_SCRIPT_FILENAME
end

Instance Method Details

#adminString

Returns:

  • (String)


407
408
409
# File 'lib/xolo/server/title.rb', line 407

def admin
  session[:admin]
end

#createvoid

This method returns an undefined value.

Save a new title, adding to the local filesystem, Jamf Pro, and the Title Editor as needed



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
# File 'lib/xolo/server/title.rb', line 664

def create
  lock

  @current_action = :creating

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

  # this will create the title as needed in the Title Editor
  create_title_in_ted
  create_title_in_jamf

  # save to file last, because saving to TitleEd and Jamf will
  # add some data
  progress 'Saving title data to Xolo server'
  save_local_data

  log_change msg: 'Title Created'

  # ssvc icon is uploaded in a separate process, and the
  # title data file will be updated as needed then.
ensure
  unlock
end

#creating?Boolean

Returns Are we creating this title?.

Returns:

  • (Boolean)

    Are we creating this title?



413
414
415
# File 'lib/xolo/server/title.rb', line 413

def creating?
  current_action == :creating
end

#deletevoid

This method returns an undefined value.

Delete the title and all of its version



900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
# File 'lib/xolo/server/title.rb', line 900

def delete
  lock
  @current_action = :deleting

  progress "Deleting all versions of #{title}...", log: :debug
  # Delete them in reverse order (oldest first) so the jamf server doesn't
  # see each older version as being 'released' again as newer
  # ones are deleted.
  version_objects.reverse.each do |vers|
    # vers might be nil if it was already deleted
    # e.g. a prev. attempt to delete the title failed partway through
    vers&.delete update_title: false
  end

  delete_title_from_ted

  delete_title_from_jamf

  delete_changelog

  progress "Deleting Xolo server data for title '#{title}'", log: :info
  title_dir.rmtree
ensure
  unlock
end

#delete_version_script_filevoid

This method returns an undefined value.

Delete the version script file



889
890
891
892
893
894
895
# File 'lib/xolo/server/title.rb', line 889

def delete_version_script_file
  return unless version_script_file.file?

  log_debug "Deleting version script file: #{version_script_file}"

  version_script_file.delete
end

#deleting?Boolean

Returns Are we deleting this title?.

Returns:

  • (Boolean)

    Are we deleting this title?



431
432
433
# File 'lib/xolo/server/title.rb', line 431

def deleting?
  current_action == :deleting
end

#deprecate_or_skip_version(vobj) ⇒ void

This method returns an undefined value.

Deprecate or skip a version

Parameters:



1020
1021
1022
1023
1024
1025
1026
1027
1028
# File 'lib/xolo/server/title.rb', line 1020

def deprecate_or_skip_version(vobj)
  # don't do anything if the status is already deprecated or skipped

  # but if its released, we need to deprecate it
  vobj.deprecate if vobj.status == Xolo::Server::Version::STATUS_RELEASED

  # and skip it if its in pilot
  vobj.skip if vobj.status == Xolo::Server::Version::STATUS_PILOT
end

#generate_uninstall_script(script_or_pkg_ids) ⇒ String, Array

Returns The uninstall script, provided or generated from the given pkg ids.

Parameters:

  • script_or_pkg_ids (String)

    The new uninstall script, or comma-separated list of pkg IDs

Returns:

  • (String, Array)

    The uninstall script, provided or generated from the given pkg ids



573
574
575
576
577
578
# File 'lib/xolo/server/title.rb', line 573

def generate_uninstall_script(script_or_pkg_ids)
  # Its already a script, validated by xadm to start with #!
  return script_or_pkg_ids if script_or_pkg_ids.is_a? String

  uninstall_script_template.sub 'PKG_IDS_FROM_XOLO_GO_HERE', script_or_pkg_ids.join(' ')
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



465
466
467
# File 'lib/xolo/server/title.rb', line 465

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

#jamf_normal_ea_nameString

Returns The display name of a version script as a normal EA in Jamf, which can be used in Smart Groups and Adv Searches.

Returns:

  • (String)

    The display name of a version script as a normal EA in Jamf, which can be used in Smart Groups and Adv Searches.



595
596
597
# File 'lib/xolo/server/title.rb', line 595

def jamf_normal_ea_name
  @jamf_normal_ea_name ||= self.class.jamf_normal_ea_name title
end

#lockObject

Lock this title for updates

Raises:

  • (Xolo::ServerError)


1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
# File 'lib/xolo/server/title.rb', line 1106

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

  while locked?
    log_debug "Waiting for update lock on title '#{title}'..." if (Time.now.to_i % 5).zero?
    sleep 0.33
  end
  Xolo::Server.object_locks[title] ||= { versions: {} }

  exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
  Xolo::Server.object_locks[title][:expires] = exp
  log_debug "Locked title '#{title}' for updates until #{exp}"
end

#locked?Boolean

Is this title locked for updates?

Returns:

  • (Boolean)


1100
1101
1102
# File 'lib/xolo/server/title.rb', line 1100

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

#patch_report_rsrcString

Returns The URL path for the patch report for this title.

Returns:

  • (String)

    The URL path for the patch report for this title



655
656
657
# File 'lib/xolo/server/title.rb', line 655

def patch_report_rsrc
  @patch_report_rsrc ||= "#{JPAPI_PATCH_TITLE_RSRC}/#{jamf_patch_title_id}/#{JPAPI_PATCH_REPORT_RSRC}"
end

#prepend_version(version) ⇒ void

This method returns an undefined value.

prepend a new version to the version_order

Parameters:

  • version (String)

    the version to prepend



605
606
607
608
609
610
611
# File 'lib/xolo/server/title.rb', line 605

def prepend_version(version)
  lock
  version_order.unshift version
  save_local_data
ensure
  unlock
end

#progress(msg, log: :debug) ⇒ 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.



451
452
453
# File 'lib/xolo/server/title.rb', line 451

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

#release(version_to_release) ⇒ void

This method returns an undefined value.

Release a version of this title

Parameters:

  • version_to_release (String)

    the version to release



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

def release(version_to_release)
  lock
  @current_action = :releasing
  @releasing_version = version_to_release

  validate_release(version_to_release)

  progress "Releasing version #{version_to_release} of title '#{title}'", log: :info

  update_versions_for_release version_to_release

  # update the title
  self.released_version = version_to_release
  save_local_data
ensure
  unlock
end

#release_version(vobj, rollback:) ⇒ void

This method returns an undefined value.

release a specific version

Parameters:



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/xolo/server/title.rb', line 1003

def release_version(vobj, rollback:)
  vobj.release rollback: rollback

  # update the jamf_manual_install_released_policy to install this version
  msg = "Jamf: Setting policy #{jamf_manual_install_released_policy_name} to install the package for version '#{vobj.version}'"
  progress msg, log: :info

  pol = jamf_manual_install_released_policy
  pol.package_ids.each { |pid| pol.remove_package pid }
  pol.add_package vobj.jamf_pkg_id
  pol.save
end

#releasing?Boolean

Returns Are we releasing a version this title?.

Returns:

  • (Boolean)

    Are we releasing a version this title?



437
438
439
# File 'lib/xolo/server/title.rb', line 437

def releasing?
  current_action == :releasing
end

#remove_version(version) ⇒ void

This method returns an undefined value.

remove a version from the version_order

Parameters:

  • version (String)

    the version to remove



619
620
621
622
623
624
625
# File 'lib/xolo/server/title.rb', line 619

def remove_version(version)
  lock
  version_order.delete version
  save_local_data
ensure
  unlock
end

#repair(repair_versions: false) ⇒ void

This method returns an undefined value.

Repair this title, and optionally all of its versions.

Look at the Title Editor title object, and ensure it’s correct based on the local data file.

- display name
- publisher
- EA or app-data
  - ea name 'xolo-<title>'
- requirement criteria
- stub version if needed
- enabled

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

- Accept Patch EA
- Normal EA 'xolo-<title>-installed-version'
- title-installed smart group 'xolo-<title>-installed'
- frozen static group 'xolo-<title>-frozen'
- manual/SSvc install-current-release policy 'xolo-<title>-install'
  - trigger 'xolo-<title>-install'
  - ssvc icon
  - ssvc category
  - description
- if uninstallable
  - uninstall script 'xolo-<title>-uninstall'
  - uninstall policy 'xolo-<title>-uninstall'
  - if expirable
    - expire policy 'xolo-<title>-expire'
      - trigger  'xolo-<title>-expire'

Parameters:

  • repair_versions (Boolean) (defaults to: false)

    run the repair method on all versions?



1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
# File 'lib/xolo/server/title.rb', line 1079

def repair(repair_versions: false)
  lock
  @current_action = :repairing
  chg_log_msg = repair_versions ? 'Repairing title and all versions' : 'Repairing title only'
  log_change msg: chg_log_msg

  progress "Starting repair of title '#{title}'"
  repair_ted_title
  repair_jamf_title_objects
  return unless repair_versions

  version_objects.each do |vobj|
    progress '#########'
    vobj.repair
  end
ensure
  unlock
end

#repairing?Boolean

Returns Are we repairing this title?.

Returns:

  • (Boolean)

    Are we repairing this title?



425
426
427
# File 'lib/xolo/server/title.rb', line 425

def repairing?
  current_action == :repairing
end

#reset_version_to_pilot(vobj) ⇒ void

This method returns an undefined value.

reset a version to pilot status, this happens when rolling back (releasing a version older than the current release)

Parameters:



1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
# File 'lib/xolo/server/title.rb', line 1035

def reset_version_to_pilot(vobj)
  # do nothing if its in pilot
  return if vobj.status == Xolo::Server::Version::STATUS_PILOT

  # this should be redundant with the above?
  return unless rollback

  # if we're here, we're rolling back to something older than this
  # version, and this version is currently released, deprecated or skipped.
  # We need to reset it to pilot.
  vobj.reset_to_pilot
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.



778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# File 'lib/xolo/server/title.rb', line 778

def save_local_data
  # create the dirs for the title
  title_dir.mkpath
  vdir = title_dir + Xolo::Server::Version::VERSIONS_DIRNAME
  vdir.mkpath

  save_version_script
  save_uninstall_script

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

  # do we have a stored self service icon?
  self.self_service_icon = ssvc_icon_file ? Xolo::ITEM_UPLOADED : nil

  log_debug "Saving local title data to: #{title_data_file}"
  title_data_file.pix_atomic_write to_json
end

#save_ssvc_icon(tempfile, orig_filename) ⇒ void

This method returns an undefined value.

Save the self_service_icon from the upload tmpfile to the file in the data dir.

This is run by the upload route, not the create or update methods here. xadm does the upload after creating or updating the title

Parameters:

  • tempfile (Pathname)

    The path to the uploaded tmp file



860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'lib/xolo/server/title.rb', line 860

def save_ssvc_icon(tempfile, orig_filename)
  lock
  # here's where we'll store it on the server
  ext_for_file = orig_filename.split(Xolo::DOT).last
  new_basename =  "#{SELF_SERVICE_ICON_FILENAME}.#{ext_for_file}"
  new_icon_file = title_dir + new_basename

  # delete any previous icon files
  existing_icon_file = ssvc_icon_file
  if existing_icon_file&.file?
    log_debug "Deleting older icon file: #{existing_icon_file.basename}"
    existing_icon_file.delete
  end

  log_debug "Saving self_service_icon '#{orig_filename}' to: #{new_basename}"
  tempfile.rename new_icon_file

  # the json file only stores 'uploaded' in the self_service_icon
  # attr.
  self.self_service_icon = Xolo::ITEM_UPLOADED
  save_local_data
ensure
  unlock
end

#save_uninstall_scriptvoid

This method returns an undefined value.

Save our current uninstall script out to our local file.

This won’t delete the script if it’s being removed, that happens elsewhere.

This overwrites the existing data.



829
830
831
832
833
834
835
836
837
838
839
# File 'lib/xolo/server/title.rb', line 829

def save_uninstall_script
  return if uninstall_script == Xolo::ITEM_UPLOADED || uninstall_ids == Xolo::ITEM_UPLOADED
  return if uninstall_script_contents.nil?

  log_debug "Saving uninstall script to: #{uninstall_script_file}"
  uninstall_script_file.pix_atomic_write uninstall_script_contents

  # the json file only stores 'uploaded' in uninstall_script
  # The actual script is saved in its own file.
  self.uninstall_script &&= Xolo::ITEM_UPLOADED
end

#save_version_scriptvoid

This method returns an undefined value.

Save our current version script out to our local file, but only if we aren’t using app_name and app_bundle_id and only if it’s changed

This won’t delete the script if it’s being removed, that happens elsewhere.

This overwrites the existing data.



809
810
811
812
813
814
815
816
817
818
# File 'lib/xolo/server/title.rb', line 809

def save_version_script
  return if app_name || app_bundle_id
  return if version_script_contents.nil?

  log_debug "Saving version_script to: #{version_script_file}"
  version_script_file.pix_atomic_write version_script_contents

  # the json file only stores 'uploaded' in the version_script attr.
  self.version_script = Xolo::ITEM_UPLOADED
end

#sessionHash

Returns:

  • (Hash)


400
401
402
403
# File 'lib/xolo/server/title.rb', line 400

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

#ssvc_icon_filePathname

Returns The the local file containing the self-service icon.

Returns:

  • (Pathname)

    The the local file containing the self-service icon



485
486
487
# File 'lib/xolo/server/title.rb', line 485

def ssvc_icon_file
  @ssvc_icon_file ||= self.class.ssvc_icon_file title
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



458
459
460
# File 'lib/xolo/server/title.rb', line 458

def ted_cnx
  server_app_instance.ted_cnx
end

#title_data_filePathname

The title data file for this title on the server

Returns:

  • (Pathname)


479
480
481
# File 'lib/xolo/server/title.rb', line 479

def title_data_file
  @title_data_file ||= self.class.title_data_file title
end

#title_dirPathname

The title dir for this title on the server

Returns:

  • (Pathname)


472
473
474
# File 'lib/xolo/server/title.rb', line 472

def title_dir
  @title_dir ||= self.class.title_dir title
end

#to_hObject

Add more server-specific data to our hash



1132
1133
1134
1135
1136
1137
# File 'lib/xolo/server/title.rb', line 1132

def to_h
  hash = super
  hash[:ted_id_number] = ted_id_number
  hash[:ssvc_icon_id] = ssvc_icon_id
  hash
end

#uninstall_script_contentsString

The code of the uninstall_script , if any, considering the new data of any changes being made

Returns nil if there is no uninstall_script, or if we are in the process of deleting it.

Returns:

  • (String)

    The string contents of the uninstall_script, if any



542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# File 'lib/xolo/server/title.rb', line 542

def uninstall_script_contents
  return @uninstall_script_contents if defined? @uninstall_script_contents

  # use any new/incoming value if we have any
  # this might still be nil or an empty array if we are removing uninstallability
  curr_script = changes_for_update.dig(:uninstall_script, :new) || changes_for_update.dig(:uninstall_ids, :new)
  curr_script = nil if curr_script.pix_empty?

  # otherwise use the existing value
  curr_script ||= uninstall_script || uninstall_ids

  # now get the actual script
  @uninstall_script_contents =
    if curr_script.pix_empty?
      # removing uninstallability, or it was never added
      nil
    elsif curr_script == Xolo::ITEM_UPLOADED
      # nothing changed, use the one we have saved on disk
      uninstall_script_file.read
    else
      # this will be a new one from the changes_for_update
      generate_uninstall_script curr_script
    end

  # log_debug "Uninstall script contents: #{@uninstall_script_contents}"
  @uninstall_script_contents
end

#uninstall_script_filePathname

Returns The the local file containing the code of the uninstall script.

Returns:

  • (Pathname)

    The the local file containing the code of the uninstall script



530
531
532
# File 'lib/xolo/server/title.rb', line 530

def uninstall_script_file
  @uninstall_script_file ||= self.class.uninstall_script_file title
end

#uninstall_script_templateString

Returns The template zsh script for uninstalling via pkgutil.

Returns:

  • (String)

    The template zsh script for uninstalling via pkgutil



582
583
584
585
586
587
588
589
590
# File 'lib/xolo/server/title.rb', line 582

def uninstall_script_template
  # parent 1 = server
  # parent 2 = xolo
  # parent 3 = lib
  # parent 4 = xolo gem
  data_dir = Pathname.new(__FILE__).parent.parent.parent.parent + 'data'
  template_file = data_dir + 'uninstall-pkgs-by-id.zsh'
  template_file.read
end

#uninstallable?Boolean

are we uninstallable?

Returns:

  • (Boolean)


845
846
847
# File 'lib/xolo/server/title.rb', line 845

def uninstallable?
  uninstall_script || !uninstall_ids.pix_empty?
end

#unlockObject

Unlock this v for updates



1122
1123
1124
1125
1126
1127
1128
# File 'lib/xolo/server/title.rb', line 1122

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

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

#update(new_data) ⇒ void

This method returns an undefined value.

Update this title, updating to the local filesystem, Jamf Pro, and the Title Editor, and applying any changes to existing versions as needed.

Parameters:

  • new_data (Hash)

    The new data sent from xadm



697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
# File 'lib/xolo/server/title.rb', line 697

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 title '#{title}' for admin '#{admin}'"
  log_debug "Updating title with these changes: #{changes_for_update}"

  # 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

  # Do ted before doing things in Jamf
  update_title_in_ted
  update_title_in_jamf
  update_local_instance_values
  save_local_data

  # if we already have a version script, and it hasn't changed, the new data should
  # contain Xolo::ITEM_UPLOADED. If its nil, we shouldn't
  # have one at all and should remove the old one if its there
  delete_version_script_file unless new_data_for_update[:version_script]

  # Do This at the end - after all the versions/patches have been updated.
  # Jamf won't see the need for re-acceptance until after the title
  # (and at least one patch) have been re-enabled.
  accept_jamf_patch_ea if need_to_accept_jamf_patch_ea?

  # any new self svc icon will be uploaded in a separate process
  # and the local data will be updated again then
  #
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



750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
# File 'lib/xolo/server/title.rb', line 750

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 Title attribute '#{attr}': '#{old_val}' -> '#{new_val}'"
    send "#{attr}=", new_val
  end

  # update any other server-specific attributes here...
end

#update_versions_for_release(version_to_release) ⇒ void

This method returns an undefined value.

Update all versions when releasing one

Parameters:

  • version_to_release (String)

    the version to release



969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
# File 'lib/xolo/server/title.rb', line 969

def update_versions_for_release(version_to_release)
  # get the Version objects and figure out our starting point, but process
  # them in reverse order so that we don't have two released versions at once
  all_versions = version_objects.reverse
  vobj_to_release = all_versions.find { |v| v.version == version_to_release }
  vobj_current_release = all_versions.find { |v| v.version == released_version }

  rollback = vobj_current_release && vobj_to_release < vobj_current_release

  progress "Rolling back from version #{released_version}", log: :info if rollback

  all_versions.each do |vobj|
    # This is the one we are releasing
    if vobj == vobj_to_release
      release_version(vobj, rollback: rollback)

    # This one is older than the one we're releasing
    # so its either deprecated or skipped
    elsif vobj < vobj_to_release
      deprecate_or_skip_version(vobj)

    # this one is newer than the one we're releasing
    # revert to pilot if appropriate
    else
      reset_version_to_pilot(vobj)

    end # if vobj == vobj_to_release
  end # all_versions.each
end

#updating?Boolean

Returns Are we updating this title?.

Returns:

  • (Boolean)

    Are we updating this title?



419
420
421
# File 'lib/xolo/server/title.rb', line 419

def updating?
  current_action == :updating
end

#validate_release(version_to_release) ⇒ void

This method returns an undefined value.

are we OK releasing a given version?

Raises:

  • (Xolo::NoSuchItemError)


953
954
955
956
957
958
959
960
961
962
963
# File 'lib/xolo/server/title.rb', line 953

def validate_release(version_to_release)
  if released_version == version_to_release
    raise Xolo::InvalidDataError,
          "Version '#{version_to_release}' of title '#{title}' is already released"
  end

  return if versions.include? version_to_release

  raise Xolo::NoSuchItemError,
        "No version '#{version_to_release}' for title '#{title}'"
end

#version_object(version) ⇒ Xolo::Server::Version

instantiate a version if this title



631
632
633
634
# File 'lib/xolo/server/title.rb', line 631

def version_object(version)
  log_debug "Instantiating version #{version} from Title instance #{title}"
  server_app_instance.instantiate_version(title: self, version: version)
end

#version_objects(refresh: false) ⇒ Array<Xolo::Server::Version>

Returns An array of all current version objects NOTE: This might not be wise if hundreds of versions, but automated cleanup should help with that.

Returns:

  • (Array<Xolo::Server::Version>)

    An array of all current version objects NOTE: This might not be wise if hundreds of versions, but automated cleanup should help with that.



640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/xolo/server/title.rb', line 640

def version_objects(refresh: false)
  @version_objects = nil if refresh
  return @version_objects if @version_objects

  @version_objects = version_order.map do |v|
    version_object v
  rescue Xolo::Core::Exceptions::NoSuchItemError
    next if deleting?

    raise
  end
end

#version_script_contentsString

The code of the version script, if any, considering the new data of any changes being made

Returns nil if there is no version script, or if we are in the process of deleting it.

Returns:

  • (String)

    The string contents of the version_script, if any



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

def version_script_contents
  return @version_script_contents if defined? @version_script_contents

  curr_script =
    if changes_for_update&.key? :version_script
      # new, incoming script
      changes_for_update[:version_script][:new]
    else
      # the current attribute value, might be Xolo::ITEM_UPLOADED
      version_script
    end

  @version_script_contents =
    if curr_script.pix_empty?
      # no script, or deleting script
      nil
    elsif curr_script == Xolo::ITEM_UPLOADED
      # use the one we have saved on disk
      version_script_file.read
    else
      # this will be a new one from the changes_for_update
      curr_script
    end
end

#version_script_filePathname

Returns The the local file containing the code of the version script.

Returns:

  • (Pathname)

    The the local file containing the code of the version script



491
492
493
# File 'lib/xolo/server/title.rb', line 491

def version_script_file
  @version_script_file ||= self.class.version_script_file title
end