Class: D3::Client

Inherits:
JSS::Client
  • Object
show all
Defined in:
lib/d3/client.rb,
lib/d3/client/cli.rb,
lib/d3/client/auth.rb,
lib/d3/client/help.rb,
lib/d3/client/lists.rb,
lib/d3/client/receipt.rb,
lib/d3/client/environment.rb,
lib/d3/client/class_methods.rb,
lib/d3/client/class_variables.rb

Overview

Client

Defined Under Namespace

Modules: Help Classes: Receipt

Constant Summary collapse

DFT_NOTIFICATION_IMAGE_PATH =

Default notification_image_path Unless otherwise specified in /etc/d3.conf, this is the default directory that can be populated with image(s) to display randomly alongside user notifications.

D3::SUPPORT_DIR + "notification_images"
ACTIONS =

NOTE: In Ruby 2.0 and up, Hashes are ordered in the order their elements are defined or added to the hash. So, the order here will affect the help output.

{
  install: {
    :aka => :i,
    :help => "install the currently live version of the given basename(s)",
    :needs_admin => true,
    :arg => :"basename or edition",
    :needs_connection => true
  },
  uninstall: {
    :aka => :u,
    :help => "uninstall the given basename(s)",
    :arg => :basename,
    :needs_connection => true
  },
  dequeue: {
    :aka => :dq,
    :help => "remove a pending puppytime (logout) pkg. Use 'all' to clear the queue",
    :arg => :basename
  },
  sync: {
    :aka => :s,
    :help => "update installed pkgs, install new auto-installed ones",
    :needs_connection => true
  },
  freeze: {
    :aka => :f,
    :help => "stop auto-updates of this basename during sync",
    :needs_connection => false,
    :arg => :basename
  },
  thaw: {
    :aka => :t,
    :help => "resume auto-updates of this basename during sync",
    :needs_connection => false,
    :arg => :basename
  },
  forget: {
    :aka => :fg,
    :help => "Remove receipt but don't try uninstalling.",
    :needs_connection => false,
    :arg => :basename
  },
  list_available: {
    :aka => :la,
    :help => "list all available live installers on the server",
    :needs_connection => true
  },
  list_installed: {
    :aka => :li,
    :help => "list all installed d3 pkgs on this machine",
    :needs_root => false
  },
  list_manual: {
    :aka => :lm,
    :help => "list all d3 pkgs on this machine not auto-installed",
    :needs_root => false
  },
  list_pilots: {
    :aka => :lp,
    :help => "list pkgs currently in pilot on this machine",
    :needs_root => false
  },
  list_frozen: {
    :aka => :lf,
    :help => "list pkgs currently frozen on this machine",
    :needs_root => false
  },
  list_puppies: {
    :aka => :lq,
    :help => "list any queued pkgs awaiting puppytime at logout",
    :needs_root => false
  },
  list_queue: {
    :aka => :lq,
    :help => "list any queued pkgs awaiting puppytime at logout",
    :needs_root => false
  },
  list_details: {
    :aka => :ld,
    :help => "show detailed info about packages in d3",
    :arg =>  :"basename or edition",
    :needs_connection => true
  },
  list_files: {
    :aka => :ls,
    :help => "list the files installed by the given editions",
    :arg => :"basename or edition" ,
    :needs_connection => true
  },
  query_file: {
   :aka => :qf,
   :help => "list any pkgs that install the given path(s)",
   :arg => :path,
   :needs_connection => true
  },
  help: {
   :aka => :h,
   :help => "show this help text",
   :needs_root => false
  },
  version: {
   :aka => :v,
   :help => "show the current versions of d3, and its libraries",
   :needs_root => false
  }
}
OPTIONS =

end COMMANDS

{

  help: {
    :cli => ['--help','-H', "-h", GetoptLong::NO_ARGUMENT ],
    :help => "show this help text"
  },

  version: {
    :cli => ['--version', '-V', GetoptLong::NO_ARGUMENT ],
    :help => "show the version of d3"
  },

  quiet: {
    :cli => ['--quiet','-q', GetoptLong::NO_ARGUMENT ],
    :help => "spew less to stdout. Use up to 3 times to suppress more output."
  },

  verbose: {
    :cli => ['--verbose','-v', GetoptLong::NO_ARGUMENT ],
    :help => "give more detail to stdout"
  },

  puppies: {
    :cli => ['--puppies', '-p', GetoptLong::NO_ARGUMENT ],
    :help => "do puppy installs immediately, instead of queuing"
  },

  force: {
    :cli => ['--force', '-f', GetoptLong::NO_ARGUMENT ],
    :help => "force d3 to perform unnatural acts"
  },

  freeze: {
    :cli => ['--freeze', '-F', GetoptLong::NO_ARGUMENT ],
    :help => "with 'install', freeze receipt immediately"
  },

  admin: {
    :cli => ['--admin', '-a', GetoptLong::REQUIRED_ARGUMENT ],
    :arg => 'admin',
    :help => "who is doing something with d3?"
  },

  no_logout_notice: {
    :cli => ['--no-puppy-notification', '-N', GetoptLong::NO_ARGUMENT ],
    :help => "don't ask the user to log out for puppies"
  },

  expiration: {
    :cli => ['--expiration', '-e', GetoptLong::REQUIRED_ARGUMENT ],
    :arg => "days",
    :help => "set a custom expiration period to the installed pkgs."
  },

  debug: {
    :cli => ['--debug', '-d', GetoptLong::NO_ARGUMENT ],
    :help => "be as verbose as possible to both std and the log."
  }
}
ENV_STATES =

Environment Vars to set during certain processes

{

  # set to 1 during a sync
  sync: 'D3_SYNCING',

  # set to 1 if --force was used
  force: "D3_FORCE",

  # set to the admin name running d3
  admin: "D3_ADMIN",

  # set to 1 if client was set to debug mode.
  debug: "D3_DEBUG",

  # set to 1 if an install is an auto-install
  auto_install: "D3_AUTO_INSTALLING",

  # set to 1 if an install is an auto-update
  auto_update: "D3_AUTO_UPDATING",

  # set to the edition of the rcpt being UNinstalled
  uninstalling_before_install: "D3_UNINSTALLING_BEFORE_INSTALL",

  # set to the edition of the pkg being installed
  pre_install: "D3_RUNNING_PRE_INSTALL",

  # set to the edition of the pkg being installed
  installing: "D3_INSTALLING",

  # set to the status of the pkg being installed
  pkg_status: "D3_PKG_STATUS",

  # set to the edition of the pkg being installed
  post_install: "D3_RUNNING_POST_INSTALL",

  # set to the edition of the pkg being expired
  expiring: "D3_EXPIRING_PKG",

  # set to a space-separated list if editions expired, if any
  # when the expiration policy is runing
  finished_expirations: "D3_FINISHED_EXPIRATIONS",

  # set to the edition of the pkg being uninstalled
  pre_remove: "D3_RUNNING_PRE_REMOVE",

  # set to the edition of the pkg being uninstalled
  removing: "D3_UNINSTALLING",

  # set to the edition of the pkg being uninstalled
  post_remove: "D3_RUNNING_POST_REMOVE",

  # set to a space-separated list if editions of items in the puppy-queue
  # when the puppytime notification policy is running
  puppytime_notification: "D3_NOTIFYING_PUPPIES",

  # set to 1 during logout-installation of pkgs requiring reboot
  puppytime: "D3_RUNNING_PUPPYTIME",

  # set to 1 during the puppy-reboot-policy
  # (generally this means a logout is happening and a reboot will happen)
  puppytime_reboot: "D3_REBOOTING_PUPPIES"

}
@@computer_groups =

Class Variables #################

nil
@@available_pkg_ids =
nil
@@editions_expired =

The editions that were expired during an expiration run will be reset to [] after the expiration process is finished including running any expiration policy.

[]
@@puppy_notification_ok_with_admin =
true
@@cloud_dist_url =
:unknown

Class Method Summary collapse

Class Method Details

.available_pkg_ids(refresh = false) ⇒ Array<Integer>

An array of package ids that are available for installing or piloting (i.e. not excluded, right OS, right cpu) for this machine.

Parameters:

  • refresh (Boolean) (defaults to: false)

    re-read the data from the server?

Returns:

  • (Array<Integer>)


716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# File 'lib/d3/client/class_methods.rb', line 716

def self.available_pkg_ids(refresh = false)
  @@available_pkg_ids = nil if refresh
  return @@available_pkg_ids if @@available_pkg_ids

  computer_groups(:refresh) if refresh

  my_cpu = `/usr/bin/uname -p`
  my_os = `/usr/bin/sw_vers -productVersion`.chomp

  @@available_pkg_ids = []

  D3::Package.package_data.values.each do |pkg|
    next unless JSS.os_ok? pkg[:oses], my_os
    next unless JSS.processor_ok? pkg[:required_processor], my_cpu
    @@available_pkg_ids << pkg[:id] if (pkg[:excluded_groups] & computer_groups).empty?
  end # do pkg
  @@available_pkg_ids
end

.clean_doghousevoid

This method returns an undefined value.

remove any invalid puppies from the queue invalid = id is no longer in d3, or status is missing



360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/d3/client/class_methods.rb', line 360

def self.clean_doghouse
  D3.log 'Checking for invalid puppies in the queue', :warn
  D3::PUPPY_Q.pending_puppies.each do |basename, pup|
    unless D3::Package.all_ids.include? pup.id
      D3.log "Removing #{pup.edition} from puppy queue: no longer in d3", :info
      D3::PUPPY_Q - pup
      next
    end
    if D3::Package.missing_data.keys.include? pup.id
      D3.log "Removing #{pup.edition} from puppy queue: status is 'missing'", :info
      D3::PUPPY_Q - pup
    end
  end
end

.clean_missing_receiptsObject

remove any receipts for packages that are missing from the server



449
450
451
452
453
454
455
456
# File 'lib/d3/client/class_methods.rb', line 449

def self.clean_missing_receipts
  D3.log 'Checking for receipts no longer in d3', :warn
  D3::Client::Receipt.all.values.select { |r| r.status == :missing }.each do |mrcpt|
    D3.log "Removing receipt for missing edition #{mrcpt.edition}", :info
    D3::Client::Receipt.remove_receipt mrcpt.basename
    D3.log "Removed receipt for missing edition #{mrcpt.edition}", :info
  end
end

.cloud_dist_point_to_use(refresh = false, pkg: nil) ⇒ String?

The cloud dist point to use for installs Returns a URL if:

- current dist point isn't reachable for downloads
- D3::CONFIG.client_try_cloud_distpoint is true
- a Cloud Dist Point is defined in the JSS

otherwise nil

Returns:

  • (String, nil)

    The download url for the cloud dist point, if we should use one, or nil.



758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# File 'lib/d3/client/class_methods.rb', line 758

def self.cloud_dist_point_to_use(refresh = false, pkg: nil)
  raise 'You must provide a pkg' unless pkg.is_a? D3::Package

  @@cloud_dist_url == :unknown if refresh

  mdp = JSS::DistributionPoint.my_distribution_point

  # Should we try a cloud distribution point if the primary is unavailable ?
  unless D3::CONFIG.client_try_cloud_distpoint
    D3.log "Config is not to try cloud, using only Distribution Point '#{mdp.name}'", :info
    return @@cloud_dist_url = nil
  end

  # Can we reach the primary distribution point? Return nil if true.
  if mdp.reachable_for_download?(get_ro_pass(:http)) || mdp.reachable_for_download?(get_ro_pass(:dist))
    D3.log "Distribution Point '#{mdp.name}' is reachable, no need for cloud", :info
    return @@cloud_dist_url = nil
  end

  # Get the cloud distribution point defined in the JSS
  cloud_url = cloud_distribution_point_url
  unless cloud_url
    D3.log 'No cloud distribution URL found.', :info
    return @@cloud_dist_url = nil
  end
  # Make sure the package is available on the cloud distribution point
  pkg_available = validate_pkg_in_cloud(cloud_url, pkg) if cloud_url
  unless pkg_available
    D3.log "#{pkg.edition} is not available in the cloud", :info
    return @@cloud_dist_url = nil
  end

  @@cloud_dist_url = cloud_url
end

.cloud_distribution_point_urlString?

Is a Cloud Distribution Point available for pkg downloads? If so, return the url for downloading pkg files (the filename will be appended during install)

Returns:

  • (String, nil)


819
820
821
822
823
824
825
826
# File 'lib/d3/client/class_methods.rb', line 819

def self.cloud_distribution_point_url
  result = JSS::DB_CNX.db.query 'SELECT download_url, cdn_url FROM cloud_distribution_point'
  urls = result.fetch
  result.free
  return nil if urls.nil?
  return nil if urls[0].empty? || urls[1].empty?
  urls[0].empty? ? urls[1] : urls[0]
end

.computer_groups(refresh = false) ⇒ Array<String>

An array of JSS::ComputerGroup names to which this computer belongs

Parameters:

  • refresh (Boolean) (defaults to: false)

    re-read the data from the server?

Returns:

  • (Array<String>)

    the JSS groups to which this machine belongs



741
742
743
744
745
# File 'lib/d3/client/class_methods.rb', line 741

def self.computer_groups(refresh = false)
  @@computer_groups = nil if refresh
  return @@computer_groups if @@computer_groups
  @@computer_groups = JSS::Computer.fetch(udid: JSS::Client.udid).computer_groups
end

.connectObject

Connect to the JSS and the DB with read-only connections The connection details must be stored in the D3 and JSS CONFIG data.



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/d3/client/auth.rb', line 38

def self.connect
  jss_ro_user = D3::CONFIG.client_jss_ro_user
  jss_ro_user ||= JSS::CONFIG.api_username

  db_ro_user = D3::CONFIG.client_db_ro_user
  db_ro_user ||= JSS::CONFIG.db_username

  JSS::DB_CNX.connect :server => JSS::CONFIG.db_server_name, :user => db_ro_user, :pw => D3::Client.get_ro_pass(:db)
  JSS::API.connect :server => JSS::CONFIG.api_server_name, :user => jss_ro_user, :pw => D3::Client.get_ro_pass(:jss)

  D3::Database.check_schema_version
end

.connect_for_reportsHash<String>

Reconnect to both the API and DB with a much larger timeout, and using an alternate DB server if one is defined.

Returns:

  • (Hash<String>)

    the hostnames of the connected JSS & MySQL servers



307
308
309
310
311
312
313
314
315
# File 'lib/d3/client/lists.rb', line 307

def self.connect_for_reports
  jss_ro_user = D3::CONFIG.client_jss_ro_user
  jss_ro_user ||= JSS::CONFIG.api_username

  db_ro_user = D3::CONFIG.client_db_ro_user
  db_ro_user ||= JSS::CONFIG.db_username

  D3.connect_for_reports  jss_ro_user, get_ro_pass(:jss), db_ro_user, get_ro_pass(:db)
end

.dequeue_puppies(puppies) ⇒ void

This method returns an undefined value.

Remove one or more puppies from the puppy queue

Parameters:

  • puppies (String, Array<String>)

    the basenames to remove from the queue



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/d3/client/class_methods.rb', line 196

def self.dequeue_puppies(puppies)
  puppies = [puppies] if puppies.is_a? String
  puppies = D3::PUPPY_Q.pups if puppies.include? 'all'
  puppies.each do |pup|
    unless the_puppy = D3::PUPPY_Q.q[pup]
      D3.log "No pkg for basename '#{pup}' in the puppy queue.", :warn
      next
    end # unless
    begin
      D3.log "Removing '#{the_puppy.edition}' from the puppy queue.", :warn
      D3::PUPPY_Q - the_puppy
    rescue
      D3.log "Couldn't remove #{the_puppy.edition} from the puppy queue: #{$ERROR_INFO}", :error
    end # begin
  end
end

.disconnectObject

Disconnect from the JSS and DB



52
53
54
55
# File 'lib/d3/client/auth.rb', line 52

def self.disconnect
  JSS::API.disconnect if JSS::API.connected?
    JSS::DB_CNX.disconnect if JSS::DB_CNX.connected?
end

.do_auto_installs(options) ⇒ void

This method returns an undefined value.

Install any new live pkgs scoped for autoinstall on this machine.

Parameters:

  • options (OpenStruct)

    the options from the commandline



381
382
383
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/d3/client/class_methods.rb', line 381

def self.do_auto_installs(options)
  verbose = options.verbose
  force = options.force || D3.forced?
  D3.log 'Checking for new packages to auto-install', :warn
  D3::Client.set_env :auto_install
  begin # for ensure below
    D3::Client::Receipt.basenames :refresh

    # loop through the groups for this machine
    auto_groups = D3::Client.computer_groups.dup
    auto_groups.unshift D3::STANDARD_AUTO_GROUP
    auto_groups.each do |group|
      # this is the intersection of all pkg ids that get auto-installed
      # for the group, and all live pkg ids...
      # meaning this machine should have these pkg ids.
      live_ids_for_group = (D3::Package.live_data.keys & D3::Package.auto_install_ids_for_group(group))

      live_ids_for_group.each do |live_id|
        # skip those not available
        next unless available_pkg_ids.include? live_id

        auto_install_basename = D3::Package.live_data[live_id][:basename]

        # skip if this basename is installed already - it'll be handled with
        # the update_installed_pkgs method during sync.
        next if D3::Client::Receipt.all.keys.include? auto_install_basename

        new_pkg = D3::Package.fetch id: live_id

        if new_pkg.reboot?
          queued_id = puppy_in_queue new_pkg.basename
          if queued_id && queued_id >= new_pkg.id
            D3.log "Skipping auto-install of puppy-package #{new_pkg.edition}, there's a newer one in the queue already", :info
            next
          end # if queued_id && queued_id >= new_pkg.id
        end #  if new_pkg.reboot?

        begin
          D3.log "Auto-installing #{new_pkg.basename} for group '#{group}'", :info
          cloud = cloud_dist_point_to_use(pkg: new_pkg)
          new_pkg.install(
            admin: D3::AUTO_INSTALL_ADMIN,
            verbose: verbose,
            force: force,
            puppywalk: options.puppies,
            alt_download_url: cloud
          )
          D3.log "Auto-installed #{new_pkg.basename}", :warn
        rescue JSS::MissingDataError, JSS::InvalidDataError, D3::InstallError
          D3.log "Skipping auto-install of #{new_pkg.edition}: #{$ERROR_INFO}", :error
          D3.log_backtrace
        rescue D3::PreInstallError
          D3.log "There was an error with the pre-install script for #{new_pkg.edition}: #{$ERROR_INFO}", :error
          D3.log_backtrace
        rescue D3::PostInstallError
          D3.log "There was an error with the post-install script for #{new_pkg.edition}: #{$ERROR_INFO} NOTE: #{new_pkg.edition} was installed, but may not work.", :error
          D3.log_backtrace
        end # begin
      end # live_ids_for_group.each do |live_id|
    end # each group
  ensure
    D3::Client.unset_env :auto_install
  end
end

.do_expirations(verbose = false, force = D3.forced?) ⇒ void

This method returns an undefined value.

Expire any pkgs that are due for expiration, and if any are expired, run the expiration policy.

Parameters:

  • verbose (Boolean) (defaults to: false)

    should operations be verbose?

  • force (Boolean) (defaults to: D3.forced?)

    should operations be forced?



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/d3/client/class_methods.rb', line 682

def self.do_expirations (verbose = false, force = D3.forced?)
  @@editions_expired = []
  D3.log 'Starting expiration check', :warn

  D3::Client::Receipt.all.values.each do |rcpt|
    begin
      # rcpt.expire only does anything if expiration is appropriate.
      expired_edition = rcpt.expire verbose, force
      @@editions_expired << expired_edition if expired_edition
    rescue
      D3.log "There was an error expiring #{rcpt.edition}: #{$ERROR_INFO}", :error
      D3.log_backtrace
    end
  end

  return true if @@editions_expired.empty?

  D3::Client.set_env :finished_expirations, @@editions_expired.join(' ')

  if policy = D3::CONFIG.client_expiration_policy
    D3.run_policy policy, :expiration, verbose
  end # if D3::CONFIG.client exp policy

  D3::Client.unset_env :finished_expirations
  @@editions_expired = []
end

.do_puppy_queue_installs_from_sync(options) ⇒ Object

Do any pending puppy installs right now, because we’re syncing and –puppies option was given



618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/d3/client/class_methods.rb', line 618

def self.do_puppy_queue_installs_from_sync(options)
  return unless options.puppies
  return if D3::PUPPY_Q.q.empty?
  D3.log 'Installing all pkgs from puppy-queue during sync with --puppies', :info
  D3::PUPPY_Q.q.each do |basename, puppy|
    begin
      D3.log "Installing #{puppy.edition} from puppy-queue during sync with --puppies", :debug
      new_pkg = D3::Package.fetch id: puppy.id
      cloud = cloud_dist_point_to_use(pkg: new_pkg)
      new_pkg.install(
        admin: puppy.admin,
        verbose: options.verbose,
        force: puppy.force,
        puppywalk: true,
        expiration: puppy.expiration,
        alt_download_url: cloud
      )

      D3::PUPPY_Q - puppy
    rescue JSS::NoSuchItemError
      D3.log "Skipping install of #{new_pkg.edition} from queue:\n   no longer in d3.", :error
      D3.log_backtrace
      D3::PUPPY_Q - puppy
    rescue JSS::MissingDataError, JSS::InvalidDataError, D3::InstallError
      D3.log "Skipping install of #{new_pkg.edition} from queue: #{$ERROR_INFO}", :error
      D3.log_backtrace
    rescue D3::PreInstallError
      D3.log "There was an error with the pre-install script for #{new_pkg.edition}: #{$ERROR_INFO}", :error
      D3.log_backtrace
    rescue D3::PostInstallError
      D3.log "There was an error with the post-install script for #{new_pkg.edition}: #{$ERROR_INFO} NOTE: #{new_pkg.edition} was installed, but may not work.", :error
      D3.log_backtrace
      D3::PUPPY_Q - puppy
    end # begin
  end # each do puppy
end

.foreground_executable_pathPathname?

get the executable path of the current foreground GUI app. NOTE, if you have fast user switching on, or multi-user screensharing, this only gets the one currenly using the physical console

Returns:

  • (Pathname, nil)

    the path to the executable of the current foreground app, nil if none



847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'lib/d3/client/class_methods.rb', line 847

def self.foreground_executable_path
  lsai = '/usr/bin/lsappinfo'
  ls_app_id = `#{lsai} front`.chomp
  return nil if ls_app_id == '[ NULL ] '

  raw = `#{lsai} info -only executablepath '#{ls_app_id}'`.chomp
  return nil if raw.empty?

  path = raw.split(/=\s*"/).last
  return nil unless path
  path.chomp!('"')
  path.empty? ? nil : Pathname.new(path)
end

.forget_receipts(basenames) ⇒ void

This method returns an undefined value.

forget one or more receipts, and their matching apple pkg receipts

Parameters:

  • basenames (Array)

    the basenames of the rcpts to forget



605
606
607
608
609
610
611
612
613
# File 'lib/d3/client/class_methods.rb', line 605

def self.forget_receipts(basenames)
  basenames.each do |bn|
    rcpt = D3::Client::Receipt.all[bn]
    next unless rcpt
    rcpt.apple_pkg_ids.each { |ar| system "/usr/sbin/pkgutil --forget '#{ar}'" }
    D3::Client::Receipt.remove_receipt bn
    D3.log "Receipt for #{rcpt.edition} has been forgotten", :warn
  end
end

.freeze_receipts(basenames) ⇒ void

This method returns an undefined value.

Freeze one or more receipts

Parameters:

  • basenames (Array)

    the basenames of the rcpts to freeze



565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/d3/client/class_methods.rb', line 565

def self.freeze_receipts(basenames)
  basenames.each do |bn|
    rcpt = D3::Client::Receipt.all[bn]
    next unless rcpt
    if rcpt.frozen
      D3.log "Can't freeze receipt for #{rcpt.edition}: already frozen.", :warn
      next
    end
    rcpt.freeze
    rcpt.update
    D3.log "Freezing receipt for #{rcpt.edition}, will not auto-update during sync", :warn
  end
end

.get_admin(pkg_to_install, options) ⇒ String

Return a valid, possibly-default, admin name for installing a package. Since the admin name is stored in the packages in the puppy-q, use that one if it’s there.

Parameters:

  • pkg (D3::Package)

    the pkg being installed, which might contain an admin name

Returns:

  • (String)

    a valid admin name to use for the install



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/d3/client/class_methods.rb', line 170

def self.get_admin(pkg_to_install, options)
  # is this puppy already in the queue? If so,
  # the queue has our admin_name
  if options.puppies && D3::PUPPY_Q.q[pkg_to_install.basename]

    admin = D3::PUPPY_Q.q[pkg_to_install.basename].admin
    admin ||= D3::DFT_PUPPY_ADMIN

  # not a puppy pkg
  else
    # start with the cli options
    admin = options.admin
    # then do a lookup if no cli option
    admin ||= D3.admin

  end # if @options.puppies

  admin
end

.get_ro_pass(pw) ⇒ String?

Get a stored read-only password from a file or an executable.

Raises a JSS::UnsupportedError if the file isn’t owned by root with 0600 permissions.

NOTE: for slightly better security, don’t store the result in a variable, use this method as needed to retrieve the passwd every time you need it.

See also D3::Configuration

Parameters:

  • pw (Symbol)

    which pw to get, one of :jss, :db, :dist, :http

Returns:

  • (String, nil)

    the password, or nil if the file isn’t defiend, or doesn’t exist.

Raises:

  • (JSS::InvalidDataError)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/d3/client/auth.rb', line 75

def self.get_ro_pass (pw)
  raise JSS::InvalidDataError, "Arg must be one of :jss, :db, :dist, :http" unless [:jss, :db, :dist, :http].include? pw

  path = case pw
  when :jss then  D3::CONFIG.client_jss_ropw_path
  when :db then D3::CONFIG.client_db_ropw_path
  when :dist then D3::CONFIG.client_distpoint_ropw_path
  when :http then D3::CONFIG.client_http_ropw_path
  end # path = case

  return nil unless path

  # if the path ends with a pipe, its a command that will
  # return the desired password, so remove the pipe,
  # execute it, and return stdout from it.
  if path.end_with? "|"
    cmd = path.chomp '|'
    output = `#{cmd} 2>&1`.chomp
    return output if $CHILD_STATUS.exitstatus.zero?
    raise D3::PermissionError, "can't get client password for #{pw}: #{output}"
  end

  file = Pathname.new path
  return nil unless file.file?
  stat = file.stat
  unless ("%o" % stat.mode).end_with? "0600" and stat.uid == 0
    raise JSS::UnsupportedError, "Password file for '#{pw}' has insecure permissions, must be 0600."
  end

  # chomping an empty string removes all trailing \n's and \r\n's
  file.read.chomp('')
end

.install(pkgs, options) ⇒ void

This method returns an undefined value.

Install one or more packages from the command-line by basename, or edition (or name, filename, id)

This method is only used by manual installs (i.e. not automated via d3 sync)

Basenames always gets the live pkg for that basename

Editions install the package regardless of status.

Parameters:

  • pkgs (Array<String,Int>)

    The packages to install

  • options (OpenStruct)

    the d3 client cli options



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/d3/client/class_methods.rb', line 49

def self.install(pkgs, options)
  pkgs = [pkgs] if pkgs.is_a? String

  pkgs.each do |pkg_to_search|
    begin
      # get a D3::Package object
      desired_pkg = D3::Package.find_package(pkg_to_search)

      raise JSS::NoSuchItemError, "No d3 package matching #{pkg_to_search}" unless desired_pkg
      raise D3::InstallError, "The package for #{desired_pkg.edition} is missing from the JSS" if desired_pkg.missing?

      if options.custom_expiration
        D3.log "Sorry #{desired_pkg.edition} is not expirable. A d3 admin needs to add an expiration path.", :warn if desired_pkg.expiration_paths.empty?
        break
      end

      curr_rcpt = D3::Client::Receipt.all[desired_pkg.basename]

      # If we were asked to freeze_on_install and the currently installed is
      # same edition, freeze it anyway.
      # Use force to freeze if the currently installed is newer.
      #
      if curr_rcpt && curr_rcpt.id >= desired_pkg.id && options.freeze_on_install
        if options.force || curr_rcpt.id == desired_pkg.id
          freeze_receipts([curr_rcpt.basename]) unless curr_rcpt.frozen?
          D3.log "Freezing previously installed #{curr_rcpt.edition}", :warn
          break
        end # if options.force elsif curr_rcpt.id == desired_pkg.id
        D3.log "Cannot freeze previously installed #{curr_rcpt.edition} (#{curr_rcpt.status}) It is newer than #{desired_pkg.edition}. Use --force if needed.", :warn
        break
      end

      # many things can be forced
      # things that are defined in the pkg itself
      # (exclusions, prohibiting procs, oses, cpus)
      # are checked in the Package#install method
      # These things are the responsibility of the
      # client.
      unless options.force
        # deprecated pkgs need force for installing
        desired_pkg.check_for_deprecated
        # skipped pkgs need force for installing
        desired_pkg.check_for_skipped
        # same or newer?
        desired_pkg.check_for_newer_version
      end # unless options.force

      if curr_rcpt
        D3.log("Un-freezing #{curr_rcpt.edition} by installing #{desired_pkg.edition}", :warn) if curr_rcpt.frozen?
        if desired_pkg.id == curr_rcpt.id
          D3.log("Re-installing #{desired_pkg.edition}(#{desired_pkg.status})", :warn)
        elsif desired_pkg.id < curr_rcpt.id
          D3.log("Rolling back #{curr_rcpt.edition}(#{curr_rcpt.status}) to #{desired_pkg.edition}(#{desired_pkg.status})", :warn)
        else
          D3.log("Updating #{curr_rcpt.edition}(#{curr_rcpt.status}) to #{desired_pkg.edition}(#{desired_pkg.status})", :warn)
        end
      end # if curr rcpt

      cloud = cloud_dist_point_to_use(pkg: desired_pkg)

      desired_pkg.install(
        force: options.force,
        admin: get_admin(desired_pkg, options),
        puppywalk: options.puppies,
        expiration: options.custom_expiration,
        verbose: options.verbose,
        alt_download_url: cloud
      )

      freeze_receipts([desired_pkg.basename]) if options.freeze_on_install

      D3.log "Finished installing #{desired_pkg.edition}(#{desired_pkg.status})", :info

    rescue JSS::MissingDataError, JSS::NoSuchItemError, JSS::InvalidDataError, D3::InstallError
      D3.log "Skipping installation of #{pkg_to_search}: #{$ERROR_INFO}", :error
      D3.log_backtrace
    rescue D3::PreInstallError
      D3.log "There was an error with the pre-install script for #{desired_pkg.edition}: #{$ERROR_INFO}", :error
      D3.log_backtrace
    rescue D3::PostInstallError
      D3.log "There was an error with the post-install script for #{desired_pkg.edition}: #{$ERROR_INFO} NOTE: it was installed, but may have problems.", :error
      D3.log_backtrace
    end # begin
  end # args.each
end

.list_available(force = false) ⇒ void

This method returns an undefined value.

list currently available packages to stdout via ‘less’



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/d3/client/lists.rb', line 146

def self.list_available(force = false)

  # If using force, show all live pkgs
  if force or D3.forced?
    ids_to_show = D3::Package.live_ids
    title = "All live packages in d3 (* = installed, ^ = puppies)"

  # otherwise, only those available to this machine based on
  # excluded_groups.
  # (the intersection of all live, with all available to this machine,
  # the latter of which includes non-live)
  else
    ids_to_show = D3::Package.live_ids & D3::Client.available_pkg_ids
    title = "Live packages available for this machine (* = installed, ^ = puppies)"
  end # if force

  header = ["Basename", "Vers-Rev" , "Auto-installed on" ]

  my_rcpt_ids = D3::Client::Receipt.all.values.map{|r| r.id}

  lines = []
  ids_to_show.each do |id|
    pkg = D3::Package.package_data[id]
    bn = pkg[:basename]
    bn += "*" if my_rcpt_ids.include? id
    bn += "^" if pkg[:reboot]
    auto_grps = pkg[:auto_groups].empty? ? "-" : pkg[:auto_groups].join(',')
    lines << [bn, "#{pkg[:version]}-#{pkg[:revision]}", auto_grps ]
  end
  lines.sort_by! {|l| l[0]}
  D3.less_text D3.generate_report(lines, header_row: header, title: title)

  return true

end

.list_details(pkgs) ⇒ void

This method returns an undefined value.

Display the details about one or more pkgs and/or receipts on the local machine

Parameters:

  • pkgs (String, Array<String>)

    the pkgs to list details for.



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
299
300
# File 'lib/d3/client/lists.rb', line 257

def self.list_details(pkgs)
  pkgs = [pkgs] if pkgs.is_a? String
  pkgs.each do |item_to_match|
    begin

      # package details
      server_pkg =  D3::Package.find_package(item_to_match)
      if server_pkg
        puts "### Found package on the server matching '#{item_to_match}'\n"
        puts server_pkg.formatted_details
      else
        puts "### No package on the server matched '#{item_to_match}'"
        puts "###   (doesn't exist, or basename has no live editions)"
      end

      # receipt details
      rcpt  = D3::Client::Receipt.find_receipt item_to_match
      if rcpt
        puts
        if server_pkg
          if rcpt.edition == server_pkg.edition
            puts "### Found matching receipt for edition '#{server_pkg.edition}'\n"
          else
            puts "### Found receipt for different edition: '#{rcpt.edition}'\n"
          end # if rcpt.edition == server_pkg.edition

        else # no svr pkg
          puts "### Found receipt matching '#{item_to_match}'\n"
        end # if svr pkg

        puts rcpt.formatted_details

      else # no rcpt
        puts "### No receipt matched '#{item_to_match}'"
      end # if rcpt

    rescue
      D3.log "An error occured getting the details of #{item_to_match}:\n   #{$!}", :error
      D3.log_backtrace
      next
    end # begin

  end # args.each
end

.list_files(pkgs) ⇒ Object

list the files installed by one or more installers



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/d3/client/lists.rb', line 184

def self.list_files(pkgs)
  self.connect_for_reports
  pkgs.each do |pkg_to_match|
    begin
      found_pkg = D3::Package.find_package pkg_to_match
      unless found_pkg
        puts "Skipping '#{pkg_to_match}': no matching edition in d3"
        next
      end
      puts "Querying for files installed by '#{found_pkg.edition}'..."

      # because this is just a list of single strings
      # and doesn't need column formatting,
      # and mostly because it can be tens of thousands
      # of lines long, we're not using 'generate_report"
      # and just building a huge string to display.
      file_list = "# Files installed by #{found_pkg.edition}\n"
      file_list += "#==========================================================================\n"
      file_list += found_pkg.installed_files.join("\n")

      D3.less_text file_list

    rescue JSS::MissingDataError, JSS::InvalidDataError
      D3.log "Skipping #{item_to_match}:\n   #{$!}", :error
      D3.log_backtrace
    end # begin
  end # isntallers.each
end

.list_frozenObject

list all frozen pkgs



118
119
120
# File 'lib/d3/client/lists.rb', line 118

def self.list_frozen
  self.list_installed :frozen
end

.list_installed(what_to_list = :all) ⇒ void

This method returns an undefined value.

list installed d3 items. The arg can be used to limit what’s listed and can be one of :all, :manual, :pilot, :frozen. (anything other than :manual, :pilot, or :frozen is treated as :all)

Parameters:

  • what_to_list (Symbol) (defaults to: :all)

    one of :all, :manual, :pilot, :frozen. defaults to :all



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/d3/client/lists.rb', line 50

def self.list_installed (what_to_list = :all)

  case what_to_list
    when :manual then
      title = "Manually installed packages (not-uninstallable<, frozen^)"
      kind = "manually installed "
    when :pilots then
      title = "Packages being piloted on this machine (not-uninstallable<, frozen^)"
      kind = "pilot "
    when :frozen then
      title = "Packages frozen on this machine (not-uninstallable<)"
      kind = "frozen "
   else
      title = "All packages installed by d3 (not-uninstallable<, frozen^)"
      kind = ""
 end # case show

  colheaders = %w{Basename Vers-Rev Status Installed By}

  lines = []

  D3::Client::Receipt.all.keys.sort.each do |bn|

    # its a d3 installer
    rcpt = D3::Client::Receipt.all[bn]

    if what_to_list == :manual
      next unless rcpt.manual?
    end # if manual

    if what_to_list == :pilots
      next unless rcpt.pilot?
    end

    if what_to_list == :frozen
      next unless rcpt.frozen?
    end

    basename = rcpt.basename
    basename += "<" unless rcpt.removable?
    basename += "^" if rcpt.frozen? and (not what_to_list == :frozen)
    date = rcpt.installed_at.strftime "%Y-%m-%d"

    lines << [basename, "#{rcpt.version}-#{rcpt.revision}", rcpt.status, date, rcpt.admin]

  end # installed_pkgs.each

  if lines.empty?
    puts "No #{kind}receipts on this computer"
  else
    D3.less_text D3.generate_report(lines, :title => title, :header_row => colheaders)
  end
end

.list_manualObject

list all manually installed pkgs



106
107
108
# File 'lib/d3/client/lists.rb', line 106

def self.list_manual
  self.list_installed  :manual
end

.list_pending_puppiesObject

list_pending_puppies



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/d3/client/lists.rb', line 124

def self.list_pending_puppies
  if  D3::PUPPY_Q.q.empty?
    puts "# There are no puppies in the queue"
    return nil
  end

  title = "Puppy packages awaiting logout"
  colheaders = %w{Edition Status Queued-at By}
  lines = []

  # loop through the puppies in the queue
  D3::PUPPY_Q.q.values.each do | pup|
    lines << [pup.edition, pup.status, (pup.queued_at.strftime "%b %d %Y %H:%M:%S"), pup.admin]
  end # do pup
  D3.less_text D3.generate_report(lines, :title => title, :header_row => colheaders)

end

.list_pilotsObject

list all installed pilots



112
113
114
# File 'lib/d3/client/lists.rb', line 112

def self.list_pilots
  self.list_installed :pilots
end

.puppy_in_queue(basename) ⇒ Integer?

Given a basename, is any edition of it in the puppy queue? If so, return the id of the queued pkg, else return nil

Parameters:

  • basename (String)

Returns:

  • (Integer, nil)

    The id of the queued package for the basename, if any



835
836
837
838
839
# File 'lib/d3/client/class_methods.rb', line 835

def self.puppy_in_queue(basename)
  pup = D3::PUPPY_Q.queue[basename]
  return nil unless pup
  pup.id
end

.puppy_notification_ok_with_admin=(bool) ⇒ void

This method returns an undefined value.

Set the –no-puppy-notification option as given by the admin

Parameters:

  • bool (Boolean)

    did the admin give the option?



669
670
671
# File 'lib/d3/client/class_methods.rb', line 669

def self.puppy_notification_ok_with_admin= (bool)
  @@puppy_notification_ok_with_admin = bool
end

.puppy_notification_ok_with_admin?Boolean

Was the –no-puppy-notification option given by the admin?

Returns:

  • (Boolean)


659
660
661
# File 'lib/d3/client/class_methods.rb', line 659

def self.puppy_notification_ok_with_admin?
  @@puppy_notification_ok_with_admin
end

.query_files(paths) ⇒ Object

find out which editions install one or more given files



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/d3/client/lists.rb', line 215

def self.query_files (paths)
  self.connect_for_reports
  paths.each do |path|
    puts "Querying for packages that install '#{path}'..."
    path = path.chomp "/"  # remove trailing slashes on dirs
    query = <<-ENDQ
    SELECT pkgs.package_id
    FROM #{D3::Package::P_TABLE[:table_name]} pkgs
    JOIN #{D3::Package::PKG_CONTENTS_TABLE} contents
    ON pkgs.#{D3::Package::P_FIELDS[:id][:field_name]} = contents.#{D3::Package::P_FIELDS[:id][:field_name]}
    WHERE contents.file = '#{Mysql.quote path}'
    ENDQ
    search_results = JSS::DB_CNX.db.query query
    ids = []
    search_results.each{|id| ids << id[0].to_i}
    search_results.free
    if ids.empty?
      puts "# Nothing in d3 installs '#{path}'"
    else
      title = "Packages that install '#{path}'"
      colheader = %w{Edition Status Installed}
      lines = []
      ids.each do |id|
        begin
          pkg = D3::Package.fetch id: id
          lines << [pkg.edition, pkg.status.to_s, (pkg.installed? ? "yes" : "no")]
        rescue
          D3.log "Couldn't get pkg for id #{id}", :error
        end # begin
      end
      D3.less_text D3.generate_report(lines, header_row: colheader, title: title)

    end # if ids.empty?
  end # paths each path
end

.set_env(var, value = 1) ⇒ void

This method returns an undefined value.

Set an ENV variable to the ‘set’ state, usually ‘1’

Parameters:

  • var (Symbol)

    which var to set, one of the keys of ENV_STATES

  • value (#to_s) (defaults to: 1)

    the value for the var, defaults to ‘1’

Raises:

  • (JSS::InvalidDataError)


110
111
112
113
# File 'lib/d3/client/environment.rb', line 110

def self.set_env (var, value = 1)
  raise JSS::InvalidDataError, "var must be one of: :#{ENV_STATES.keys.join(' :')}" unless ENV_STATES.keys.include? var
  ENV[ENV_STATES[var]] = value.to_s
end

.sync(options = OpenStruct.new) ⇒ void

This method returns an undefined value.

Sync this machine

Parameters:

  • options (OpenStruct) (defaults to: OpenStruct.new)

    the options from the commandline



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/d3/client/class_methods.rb', line 219

def self.sync(options = OpenStruct.new)
  D3::Client.set_env :sync
  D3.log 'Starting sync', :warn

  begin
    # update rcpts
    update_rcpts

    # clean out any invalid puppies from the queue
    clean_doghouse

    # install puppies now?
    do_puppy_queue_installs_from_sync options

    # updates/patches?
    update_installed_pkgs options

    # new auto-installs?
    do_auto_installs options

    # expirations
    do_expirations

    # removie receipts w/ missing packages on the server
    # This must happen AFTER update_installed_pkgs
    # so that the basename gets any updates on the server
    # before removing the recetip (which wouild prevent
    # updates)
    clean_missing_receipts

    D3.log 'Finished sync', :warn
  ensure
    D3::Client.unset_env :sync
  end
end

.thaw_receipts(basenames) ⇒ void

This method returns an undefined value.

Thaw one or more receipts

Parameters:

  • basenames (Array)

    the basenames of the rcpts to thaw



585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/d3/client/class_methods.rb', line 585

def self.thaw_receipts(basenames)
  basenames.each do |bn|
    rcpt = D3::Client::Receipt.all[bn]
    next unless rcpt
    unless rcpt.frozen
      D3.log "Can't thaw receipt for #{rcpt.edition}: not frozen.", :warn
      next
    end
    rcpt.thaw
    rcpt.update
    D3.log "Thawing receipt for #{rcpt.edition}, will resume auto-update during sync", :warn
  end
end

.uninstall(rcpts, options) ⇒ void

This method returns an undefined value.

uninstall one or more packages from the commandline

Parameters:

  • pkgs (Array<String,Int>)

    The packages to uninstall



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/d3/client/class_methods.rb', line 141

def self.uninstall(rcpts, options)
  rcpts = [rcpts] if rcpts.is_a? String
  rcpts.each do |rcpt_to_remove|
    begin

      rcpt = D3::Client::Receipt.find_receipt rcpt_to_remove
      raise D3::UninstallError, "No receipt for '#{rcpt_to_remove}', can't uninstall." unless rcpt

      D3.log "Uninstalling #{rcpt.edition}...", :info
      rcpt.uninstall options.verbose, options.force
      D3.log "Finished uninstalling #{rcpt.edition}.", :info

    rescue JSS::MissingDataError, D3::UninstallError, JSS::InvalidDataError
      D3.log "Skipping uninstall of #{rcpt_to_remove}: #{$ERROR_INFO}", :error
      D3.log_backtrace
      next
    end # begin
  end # rcpts.each
end

.unset_all_envvoid

This method returns an undefined value.

Unset all ENV vars



130
131
132
# File 'lib/d3/client/environment.rb', line 130

def self.unset_all_env
  ENV_STATES.keys.each{|v| ENV[ENV_STATES[v]] = nil }
end

.unset_env(var) ⇒ void

This method returns an undefined value.

Unset an ENV variable

Parameters:

  • var (Symbol)

    which var to unset, one of the keys of ENV_STATES

Raises:

  • (JSS::InvalidDataError)


121
122
123
124
# File 'lib/d3/client/environment.rb', line 121

def self.unset_env (var)
  raise JSS::InvalidDataError, "var must be one of: :#{ENV_STATES.keys.join(' :')}" unless ENV_STATES.keys.include? var
  ENV[ENV_STATES[var]] = nil
end

.update_installed_pkgs(options) ⇒ void

This method returns an undefined value.

Update any currently installed basenames to the currently live one skipping any basenames currently frozen

Parameters:

  • options (OpenStruct)

    the options from the commandline



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/d3/client/class_methods.rb', line 465

def self.update_installed_pkgs(options)
  verbose = options.verbose
  force = options.force || D3.forced?
  D3.log 'Checking for updates to installed packages', :warn
  D3::Client.set_env :auto_update
  begin # see ensure below

    # get the current list of live basenames and the ids of the live editions
    live_basenames_to_ids = D3::Package.basenames_to_live_ids

    # loop through the install pkgs
    D3::Client::Receipt.all.values.each do |rcpt|
      # is there a live pkg for this basename?
      if live_basenames_to_ids[rcpt.basename]
        live_id = live_basenames_to_ids[rcpt.basename]
        live_pkg_data = D3::Package.package_data[live_id]
      else
        D3.log "Skipping update for #{rcpt.edition}: no currently live package for basename", :info
        next
      end

      # are we rolling back? Not if the recpt is in pilot.
      if live_pkg_data[:id] < rcpt.id

        if rcpt.pilot?
          D3.log "Skipping rollback of #{live_pkg_data[:edition]}, #{rcpt.edition} is in pilot", :info
          next
        else
          rollback = true
        end
      # no we aren't rolling back
      else
        # skip unless the live id is higher than the rcpt id
        unless live_pkg_data[:id] > rcpt.id
          D3.log "No update for #{rcpt.edition}", :debug
          next
        end
      end

      # skip any frozen receipts
      if rcpt.frozen?
        D3.log "Skipping update check for #{rcpt.edition}(#{rcpt.status}): currently frozen on this machine.", :warn
        next
      end

      # check the puppy queue if needed
      if live_pkg_data[:reboot]
        queued_id = puppy_in_queue(live_pkg_data[:basename])
        if queued_id && queued_id >= live_pkg_data[:id]
          D3.log "Skipping auto-update of puppy-queue item #{live_pkg_data[:edition]}, there's a newer one in the queue already", :info
          next
        end # if queued_id && queued_id >= live_pkg.id
      end #  if live_pkg.reboot?

      # mention rollbacks
      if rollback
        D3.log "Rolling back #{rcpt.edition} (#{rcpt.status}) to older live #{live_pkg_data[:edition]}.", :warn
      else
        D3.log "Updating #{rcpt.edition} (#{rcpt.status}) to #{live_pkg_data[:edition]} (#{live_pkg_data[:status]})", :warn
      end

      # are we bringing over a custom expiration period?
      expiration = rcpt.custom_expiration ? rcpt.expiration : nil

      # heres the pkg
      live_pkg = D3::Package.fetch id: live_basenames_to_ids[rcpt.basename]

      begin
        cloud = cloud_dist_point_to_use(pkg: live_pkg)
        live_pkg.install(
          admin: rcpt.admin,
          expiration: expiration,
          verbose: verbose,
          force: force,
          puppywalk: options.puppies,
          alt_download_url: cloud
        )
        D3.log "Done updating #{rcpt.edition} (#{rcpt.status}) to #{live_pkg.edition} (#{live_pkg.status})", :info
      rescue JSS::MissingDataError, JSS::InvalidDataError, D3::InstallError
        D3.log "Skipping update of #{rcpt.edition} to #{live_pkg.edition}: #{$ERROR_INFO}", :error
        D3.log_backtrace
      rescue D3::PreInstallError
        D3.log "There was an error with the pre-install script for #{live_pkg.edition}: #{$ERROR_INFO}", :error
        D3.log_backtrace
      rescue D3::PostInstallError
        D3.log "There was an error with the post-install script for #{live_pkg.edition}: #{$ERROR_INFO} NOTE: #{live_pkg.edition} was installed, but may not work.", :error
        D3.log_backtrace
      end # begin
    end # D3::Client::Receipt.all.values.each
  ensure
    D3::Client.unset_env :auto_update
  end # begin..ensure
end

.update_rcptsvoid

This method returns an undefined value.

Update any receipt data that might be changed in the matching package on the server, including:

  • status

  • pre- or post-remove scripts

  • removability

  • prohibiting processes

  • expiration details

Also update

  • last usage for expiring pkgs



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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/d3/client/class_methods.rb', line 268

def self.update_rcpts
  D3.log 'Updating receipts', :warn

  D3::Client::Receipt.all.each do |basename, rcpt|
    pkgdata = D3::Package.find_package rcpt.edition, :hash

    unless pkgdata
      # if this pkg is missing, mark it so...
      D3.log "Receipt '#{rcpt.edition}' is missing from d3. Updating receipt.", :info
      rcpt.status = :missing
      rcpt.update
      next
    end
    need_update = false

    # Are we rolling back to a prev version?
    # If the pkgdata[:status] is :pilot and the
    # rcpt.status is NOT :pilot, then we are.
    rolling_back = (pkgdata[:status] == :pilot) && (rcpt.status != :pilot)

    # status
    unless rolling_back
      if rcpt.status != pkgdata[:status]
        # update the status
        rcpt.status = pkgdata[:status]
        D3.log "Updating status for #{rcpt.edition} to #{pkgdata[:status]}", :info
        need_update = true
      end # if
    end # unless

    # pre-remove script
    if rcpt.pre_remove_script_id != pkgdata[:pre_remove_script_id]
      rcpt.pre_remove_script_id = pkgdata[:pre_remove_script_id]
      D3.log "Updating pre-remove script for #{rcpt.edition}", :info
      need_update = true
    end # if

    # post-remove script
    if rcpt.post_remove_script_id != pkgdata[:post_remove_script_id]
      rcpt.post_remove_script_id = pkgdata[:post_remove_script_id]
      D3.log "Updating post-remove script for #{rcpt.edition}", :info
      need_update = true
    end # if

    # removability
    if rcpt.removable? != pkgdata[:removable]
      rcpt.removable = pkgdata[:removable]
      D3.log "Updating removability for #{rcpt.edition}", :info
      unless rcpt.removable?
        rcpt.expiration = 0
        D3.log "#{rcpt.edition} is not expirable now that it's not removable", :info
      end
      need_update = true
    end # if

    # expiration
    if rcpt.removable?

      unless rcpt.expiration_paths_match? pkgdata[:expiration_paths]
        rcpt.expiration_paths = pkgdata[:expiration_paths]
        D3.log "Updating expiration path(s) for #{rcpt.edition}", :info
        need_update = true
      end # if

      if (rcpt.expiration != pkgdata[:expiration].to_i) && !rcpt.custom_expiration
        rcpt.expiration = pkgdata[:expiration].to_i
        D3.log "Updating expiration for #{rcpt.edition}", :info
        need_update = true
      end # if
    end # if removable

    # prohibiting_processes
    # make sure they are all arrays as of v3.0.12
    unless rcpt.prohibiting_processes.is_a? Array
      rcpt.prohibiting_processes = JSS.to_s_and_a(rcpt.prohibiting_processes)[:arrayform]
      need_update = true
    end
    if rcpt.prohibiting_processes.sort != pkgdata[:prohibiting_processes].sort
      rcpt.prohibiting_processes = pkgdata[:prohibiting_processes]
      D3.log "Updating prohibiting_processes for #{rcpt.edition}", :info
      need_update = true
    end # if

    rcpt.update if need_update
  end # each do basename, rcpt
end

.validate_pkg_in_cloud(url, pkg) ⇒ Boolean

given a cloud url and a D3::Package is the pkg available at that url?

Returns:

  • (Boolean)


798
799
800
801
802
803
804
805
806
807
808
809
810
811
# File 'lib/d3/client/class_methods.rb', line 798

def self.validate_pkg_in_cloud(url, pkg)
  full_url = "#{url}/#{pkg.filename}"
  dummy_install_target = Pathname.new('/Volumes/' + Time.now.asctime)
  jamf_cmd = "#{JSS::Client::JAMF_BINARY} install -package #{Shellwords.escape pkg.filename} -path #{Shellwords.escape full_url} -target #{Shellwords.escape dummy_install_target}/ -showProgress -verbose"
  Open3.popen2e(jamf_cmd) do |_stdin, stdout_err, wait_thr|
    stdout_err.each do |line|
      if /^\d*+\.\d*+% / =~ line
        Process.kill('KILL', wait_thr.pid)
        return true
      end # end if /^\d*+\.\d*+% / =~ line
    end # end stdout_err.each do |line|
    return false
  end # end Open3.popen2e(jamf_cmd) do |_stdin, stdout_err, wait_thr|
end