Module: D3

Defined in:
lib/d3.rb,
lib/d3.rb,
lib/d3/log.rb,
lib/d3/admin.rb,
lib/d3/state.rb,
lib/d3/client.rb,
lib/d3/package.rb,
lib/d3/utility.rb,
lib/d3/version.rb,
lib/d3/basename.rb,
lib/d3/database.rb,
lib/d3/admin/add.rb,
lib/d3/constants.rb,
lib/d3/puppytime.rb,
lib/d3/admin/auth.rb,
lib/d3/admin/edit.rb,
lib/d3/admin/help.rb,
lib/d3/client/cli.rb,
lib/d3/exceptions.rb,
lib/d3/admin/prefs.rb,
lib/d3/admin/state.rb,
lib/d3/client/auth.rb,
lib/d3/client/help.rb,
lib/d3/admin/report.rb,
lib/d3/client/lists.rb,
lib/d3/admin/options.rb,
lib/d3/configuration.rb,
lib/d3/admin/validate.rb,
lib/d3/client/receipt.rb,
lib/d3/package/mixins.rb,
lib/d3/package/aliases.rb,
lib/d3/package/getters.rb,
lib/d3/package/setters.rb,
lib/d3/package/validate.rb,
lib/d3/admin/interactive.rb,
lib/d3/package/constants.rb,
lib/d3/package/questions.rb,
lib/d3/client/environment.rb,
lib/d3/package/attributes.rb,
lib/d3/package/constructor.rb,
lib/d3/client/class_methods.rb,
lib/d3/package/class_methods.rb,
lib/d3/puppytime/puppy_queue.rb,
lib/d3/client/class_variables.rb,
lib/d3/package/client_actions.rb,
lib/d3/package/server_actions.rb,
lib/d3/package/class_variables.rb,
lib/d3/package/private_methods.rb,
lib/d3/puppytime/pending_puppy.rb

Overview

The D3 module provides the foundation, and the guts, of d3. Most of the work of the executables (d3, d3admin, d3helper, puppytime) is performed here. It in turn, is built upon the JSS module, provided by the ruby-jss gem, for API and MySQL access to the JSS.

Defined Under Namespace

Modules: Admin, Basename, Database, PuppyTime Classes: Client, Configuration, InstallError, Log, Package, PermissionError, PostInstallError, PostRemoveError, PreInstallError, PreRemoveError, ScriptError, UninstallError

Constant Summary collapse

LOG =

the singleton instance of our logger

D3::Log.instance
VERSION =
'3.0.26'.freeze
SUPPORT_DIR =

where do we keep our stuff?

Pathname.new "/Library/Application Support/d3"
DFT_CLI_ADMIN =

the default name to use as the @@script_admin

"unknown"
DFT_PUPPY_ADMIN =

Shouldn’t see this ever, but if there’s no admin name for a pkg in the puppy queue, use this

"unknown-puppyq"
AUTO_INSTALL_ADMIN =

the ‘admin’ name when a pkg is auto-installed during a d3 sync

"auto-installed"
STANDARD_AUTO_GROUP =

When this word is used as an auto_group name it means that all clients should get the package automatically.

"standard"
DISALLOWED_ADMINS =

When a real admin name is needed, it cant be one of these, or those listed in D3::CONFIG.client_prohibited_admin_names or we’ll raise an exception

[nil, "", "root", DFT_CLI_ADMIN, AUTO_INSTALL_ADMIN]
REPORT_CONNECTION_TIMEOUT =

reports can take a long time to generate, lets set the timeout to a long time.

3600
DEBUG_FILE =

when this file exists, d3, d3admin are set to debug mode. This is useful when you have a d3 command embedded in some other tool, but need to get debug logging.

Pathname.new "/tmp/d3debug-on"
CONFIG =

The single instance of Configuration must be created before the LOG, since the log looks here for file names

D3::Configuration.instance
PUPPY_Q =

here’s our one queue instance

D3::PuppyTime::PuppyQueue.instance
@@loaded =

Set to true after all files are required

true
@@verbosity =

This stores the current level of log messages sent to stdout. See D3::Log.level to set the level for messages sent to the log.

D3::Log::DFT_VERBOSITY
@@force =

Have we been asked to be forceful, and perform unnatural acts? Force is used in many different was in many places so we’ll store it here and anything can access it using D3::force, D3::unforce, and D3::forced?

false

Class Method Summary collapse

Class Method Details

.adminString

Try to figure out the login name of the admin running this code

Returns:

  • (String)

    an admin name.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/d3/utility.rb', line 47

def self.admin

  no_good = self.badmins

  # use the USER if it's valid
  admin = ENV['USER']

  # otherwise, try SUDO_USER
  admin = ENV['SUDO_USER'] if no_good.include? admin

  # otherwise, try SSH_CLIENT_USER
  admin =  ENV['SSH_CLIENT_USER'] if no_good.include? admin

  # otherwise, use the default, which might still be bad
  admin = DFT_CLI_ADMIN if no_good.include? admin

  return admin
end

.badminsArray

The list of names not allowed as the –admin option in d3

This just combines DISALLOWED_ADMINS and D3::CONFIG.client_prohibited_admin_names

Returns:

  • (Array)

    list of admins not allowed.



73
74
75
76
# File 'lib/d3/utility.rb', line 73

def self.badmins
  return D3::DISALLOWED_ADMINS unless D3::CONFIG.client_prohibited_admin_names
  return D3::DISALLOWED_ADMINS + D3::CONFIG.client_prohibited_admin_names
end

.col_widths(data, header_row = []) ⇒ Array<Integer>

Given an Array of Arrays representing rows and columns of data figure out the widest width of each column and return an array of integers representing those widths

Parameters:

  • data_array (Array<Array>)

    The rows and columns of data

  • header_row (Array) (defaults to: [])

    An optional header row to include in the width calculation.

Returns:

  • (Array<Integer>)

    the max widths of each column of data.



225
226
227
228
229
230
231
232
233
234
# File 'lib/d3/utility.rb', line 225

def self.col_widths (data, header_row = [])
  widths = header_row.map{|c| c.to_s.length}
  data.each do |row|
    row.each_index do |col|
      this_width = row[col].to_s.length
      widths[col] = this_width if this_width > widths[col].to_i
    end # do field
  end # do line
  widths
end

.connect_for_reports(api_user, api_pw, db_user, db_pw) ⇒ Hash<String>

Reconnect to both the API and DB with a much larger timeout, and using an alternate DB server if one is defined. Should be used by either the D3::Client for lists, or D3::Admin for reports, with appropriate credentials.

Parameters:

  • api_user (String)

    the user for the api connection

  • api_pw (String the pw for the api user)

    pi_pw[String the pw for the api user

  • db_user (String)

    the user for the db connection

  • db_pw (String the pw for the db user)

    b_pw[String the pw for the db user

Returns:

  • (Hash<String>)

    the hostnames of the connected JSS & MySQL servers



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/d3/utility.rb', line 309

def self.connect_for_reports(api_user, api_pw, db_user, db_pw)

  JSS::API.connect :user => api_user, :pw => api_pw, :timeout => REPORT_CONNECTION_TIMEOUT

  if D3::CONFIG.report_db_server
    begin
    JSS::DB_CNX.connect(
      :server => D3::CONFIG.report_db_server,
      :user => db_user,
      :pw => db_pw,
      :timeout => REPORT_CONNECTION_TIMEOUT
      )
      return {db: D3::CONFIG.report_db_server, api: JSS::CONFIG.api_server_name}

    rescue Mysql::ServerError::AccessDeniedError
      raise JSS::AuthenticationError, "Authentication error on report_db_server: Credentials must match #{JSS::CONFIG.db_username} on #{JSS::CONFIG.db_server_name}"
    end # begin
  end # if rpt db server

  JSS::DB_CNX.connect :user => db_user, :pw => db_pw, :timeout => REPORT_CONNECTION_TIMEOUT

  return {db: JSS::CONFIG.db_server_name, api: JSS::CONFIG.api_server_name}
end

.connected?false, Hash

Are we connected to the API and DB servers?

returns false if the are not both connected returns a hash like this if both are connected:

=> “user@api_server”, :db => “sql_user@db_server”

Returns:

  • (false, Hash)

    Are we connected to the servers, and if so, what hosts and usernames



112
113
114
115
116
117
118
119
# File 'lib/d3/state.rb', line 112

def self.connected?
  return false unless loaded?
  return false unless JSS::API.connected? and JSS::DB_CNX.connected?
  return {
    :api => (JSS::API.cnx.options[:user] + "@" + JSS::API.cnx.options[:server]),
    :db => (JSS::DB_CNX.user + "@" + JSS::DB_CNX.server)
  }
end

.forcevoid

This method returns an undefined value.

Turn on force, module-wide



80
81
82
83
# File 'lib/d3/state.rb', line 80

def self.force
  @@force = true
  D3::Client.set_env :force
end

.forced?Boolean

Is force turned on, module-wide?

Returns:

  • (Boolean)


98
99
100
# File 'lib/d3/state.rb', line 98

def self.forced?
  @@force
end

.generate_report(lines, type: :fixed, header_row: [], **args) ⇒ String

Generate a report of columned data, either fixed-width or tab-delimited. The title line(s) are pre-pended with ‘# ’ for easier exclusion when using the report as input for some other program. If the :type is :fixed, so will the column header line.

Parameters:

  • lines (Array<Array>)

    the rows and columns of data

  • type (Symbol) (defaults to: :fixed)

    :fixed or :tab, defaults to :fixed

  • args (Hash)

    the options for the report

Options Hash (**args):

  • :header_row (Array<String>, nil)

    the column headers. optional.

Returns:

  • (String)

    the formatted report.

Raises:

  • (JSS::InvalidDataError)


162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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
212
# File 'lib/d3/utility.rb', line 162

def self.generate_report (lines, type: :fixed, header_row: [], **args)
  raise JSS::InvalidDataError, "The first argument must be an Array of Arrays" unless lines.is_a? Array
  raise JSS::InvalidDataError, "The header_row must be an Array" unless header_row.is_a? Array

  return "" if lines.empty?

  # tab delim is easy
  if type== :tab
    report_tab = header_row.join("\t")
    lines.each{|line| report_tab += "\n#{line.join("\t")}" }
    return report_tab.strip
  end # if :tab

  # below here, fixed width
  format = ""
  line_width = 0
  header_row[0] = "# #{header_row[0]}"

  self.col_widths(lines, header_row).each do |w|
    # make sure there's a space between columns
    col_width = w + 1

    # add the column to the printf format
    format += "%-#{col_width}s"
    line_width += col_width
  end
  format += "\n"

  # limit the total line width for the header the width of the terminal
  if IO.console
    height, width = IO.console.winsize
    line_width = width if line_width > width
  else
    line_width = 80
  end

  # title if given
  report = args[:title] ? "# #{args[:title]}\n" : ""

  unless header_row.empty?
    raise JSS::InvalidDataError, "Header row must have #{lines[0].count} items" unless header_row.count == lines[0].count
    # then the header line if given
    report +=  format % header_row
    # add a separator
    report +=  "#" + ("-" * (line_width -1))  + "\n"
  end
  # add the rows
  lines.each { |line| report += format % line }

  return report
end

.less_text(text, show_help = true) ⇒ Object

Send a string to the terminal, possibly piping it through ‘less’ if the number of lines is greater than the number of terminal lines minus 3

Parameters:

  • text (String)

    the text to send to the terminal

  • show_help (Boolean) (defaults to: true)

    should the text have a line at the top showing basic ‘less’ key commands.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/d3/utility.rb', line 247

def self.less_text (text, show_help = true)
  unless IO.console
    puts text
    return
  end

  height, width = IO.console.winsize

  if text.lines.count <= (height - 3)
    puts text
    return
  end

  if show_help
    help = "#------' ' next, 'b' prev, 'q' exit, 'h' help ------"
    text = "#{help}\n#{text}"
  end

  # point stdout through less, print, then restore stdout
  less = IO.popen("/usr/bin/less","w")
  begin
    less.puts text

  # this catches the quitting of 'less' before all the output
  # is displayed
  rescue Errno::EPIPE => e
    true
  ensure
    less.close
  end
end

.loaded?Boolean

is the module fully loaded?

Returns:

  • (Boolean)


33
34
35
# File 'lib/d3/state.rb', line 33

def self.loaded?
  @@loaded
end

.log(msg, severity = D3::Log::DFT_LOG_LEVEL) ⇒ void

This method returns an undefined value.

Log a message to the d3 log, possibly sending it to stderr as well.

The message will appear in the log:

- if the log is writable by the current user
- based upon its severity level, and the current D3::Log.level.
  Any message more severe than the log level will be logged.

The message will also appear on stderr if the message severity is at or higher than the current @@verbosity.

If the @@verbosity is :debug the messages to stderr will be prefixed with the message severity.

In the d3 command, @@verbosity is controlled with the -v, -q and -d options

See also D3::Log.log and the ruby Logger module. See also D3::verbosity=

Parameters:

  • msg (String)

    the message to log

  • severity (Symbol) (defaults to: D3::Log::DFT_LOG_LEVEL)

    the severity level of this message, defaults to D3::Log::DFT_LOG_LEVEL



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/d3/log.rb', line 55

def self.log (msg, severity = D3::Log::DFT_LOG_LEVEL)

  message_severity = D3::Log.check_level(severity)

  # send to stderr if needed
  if message_severity >= @@verbosity
    if  @@verbosity ==  D3::Log::LOG_LEVELS[:debug]
      STDERR.puts "#{severity}: #{msg}"
    else
      STDERR.puts msg
    end
  end #

  # send to the logger
  D3::Log.instance.log msg, severity
end

.log_backtrace(e = $@) ⇒ Object

Log the lines of backtrace from the most recent exception but only if the current severity is :debug



74
75
76
77
# File 'lib/d3/log.rb', line 74

def self.log_backtrace( e = $@ )
  return unless D3::LOG.level == :debug
  e.backtrace.each{|line| D3.log "   #{line}", :debug }
end

.parse_plist(plist) ⇒ Object

Parse a plist into a Ruby data structure. This enhances Plist::parse_xml taking file paths, as well as XML Strings and reading the files regardless of binary/XML format.

see JSS::parse_plist TODO - make all calls to this go directly to JSS.parse_plist

Parameters:

  • plist (Pathname, String)

    the plist XML, or the path to a plist file

Returns:

  • (Object)

    the parsed plist as a ruby hash,array, etc.



290
291
292
# File 'lib/d3/utility.rb', line 290

def self.parse_plist (plist)
  JSS.parse_plist plist
end

.policy_scriptsHash{String => Array<Integer>}

Get the ids of all scripts used by all policies This is a hash of PolicyName => Array of Script id’s

Returns:

  • (Hash{String => Array<Integer>})


128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/d3/utility.rb', line 128

def self.policy_scripts
  qry = <<-ENDQ
  SELECT p.name, GROUP_CONCAT(ps.script_id) AS script_ids
  FROM policies p
  JOIN policy_scripts ps
    ON p.policy_id = ps.policy_id
  GROUP BY p.policy_id
  ENDQ
  res = JSS::DB_CNX.db.query qry
  p_scripts = {}
  res.each{|r| p_scripts[r[0]] = r[1].split(/,\s*/).map{|id| id.to_i}  }
  p_scripts
end

.prohibited_by_process_running?(xprocs) ⇒ Boolean

Is there a process running that would prevent un/installation?

Returns:

  • (Boolean)


34
35
36
37
38
39
40
41
# File 'lib/d3/utility.rb', line 34

def self.prohibited_by_process_running? (xprocs)
    # this is needed in case saved rcpts have nil or a string instead
    # of an array, from pre v3.0.12
    xprocs = JSS.to_s_and_a(xprocs)[:arrayform]
    processes = `/bin/ps -A -c -o comm`.split("\n")
    current_prohibiting = processes & xprocs
    return true unless current_prohibiting.empty?
end

.run_policy(policy, type, verbose = false) ⇒ boolean

Run a Jamf Pro policy on the local machine

Parameters:

  • policy (String, Integer)

    the custom-trigger, name, or id of the policy

  • type (Symbol)

    the type of policy being run, e.g. :expiration

  • verbose (Boolean) (defaults to: false)

    should we be verbose?

Returns:

  • (boolean)

    Did the policy run?



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
# File 'lib/d3/utility.rb', line 88

def self.run_policy (policy, type, verbose = false)

  D3.log "Running #{type} policy", :info

  # if numeric, and there's a policy with that id
  if policy =~ /^\d+$/ and polname = JSS::Policy.map_all_ids_to(:name)[policy]
    D3.log "Executing #{type} policy '#{polname}', id: #{policy}", :debug
    pol_to_run = "-id #{policy}"

  # if there's a policy with that name
  elsif polid = JSS::Policy.map_all_ids_to(:name).invert[policy]
    D3.log "Executing #{type} policy '#{policy}', id: #{polid}", :debug
    pol_to_run = "-id #{polid}"

  # else assume its a trigger
  else
    D3.log "Executing #{type} policy with trigger '#{policy}'", :debug
    pol_to_run = "-event '#{policy}'"
  end

  output = JSS::Client.run_jamf "policy", pol_to_run, verbose
  if D3::LOG.level == :debug
    D3.log "Policy execution output:", :debug
    output.lines.each{|l| D3.log "  #{l.chomp}", :debug}
  end

  if output.include? "No policies were found for the"
    D3.log "No policy matching '#{policy}' was found in the JSS", :warn
    return false
  else
    D3.log "Done executing #{type} policy", :debug
    return true
  end
end

.unforcevoid

This method returns an undefined value.

Turn off force, module-wide



89
90
91
92
# File 'lib/d3/state.rb', line 89

def self.unforce
  @@force = false
  D3::Client.unset_env :force
end

.verbosityInteger

The current verbosity level

Returns:

  • (Integer)


45
46
47
# File 'lib/d3/state.rb', line 45

def self.verbosity
  @@verbosity
end

.verbosity=(new_val) ⇒ void

This method returns an undefined value.

Set the level of verbosity to stderr. Messages logged via D3#log, of this severity and higher, will show up on stderr They may show up in the log depending on the D3::LOG.level

Parameters:

  • new_verbosity (Symbol, Integer)

    the new value, one of D3::Log::LOG_LEVELS



58
59
60
61
62
63
64
65
66
# File 'lib/d3/state.rb', line 58

def self.verbosity= (new_val)
  # range is 0-4 if we're given an integer
  # so force it to be in the range.
  if new_val.is_a? Fixnum
    new_val = 0 if new_val < 0
    new_val = 4 if new_val > 4
  end
  @@verbosity = D3::Log.check_level(new_val)
end