xolo-server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ module Xolo
9
+
10
+ module Core
11
+
12
+ module Exceptions
13
+
14
+ # General errors
15
+
16
+ class MissingDataError < RuntimeError; end
17
+
18
+ class InvalidDataError < RuntimeError; end
19
+
20
+ class NoSuchItemError < RuntimeError; end
21
+
22
+ class UnsupportedError < RuntimeError; end
23
+
24
+ class KeychainError < RuntimeError; end
25
+
26
+ class ActionRequiredError < RuntimeError; end
27
+
28
+ # Connections & Access
29
+
30
+ class ConnectionError < RuntimeError; end
31
+
32
+ class NotConnectedError < ConnectionError; end
33
+
34
+ class TimeoutError < ConnectionError; end
35
+
36
+ class AuthenticationError < ConnectionError; end
37
+
38
+ class PermissionError < ConnectionError; end
39
+
40
+ class InvalidTokenError < ConnectionError; end
41
+
42
+ class ServerError < ConnectionError; end
43
+
44
+ # Parsing errors
45
+
46
+ class DisallowedYAMLDumpClass; end
47
+
48
+ end # module Exceptions
49
+
50
+ end # module Core
51
+
52
+ end # module Xolo
@@ -0,0 +1,43 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ # constants and methods for consistent JSON processing on the server
16
+ module JSONWrappers
17
+
18
+ # when this module is extended
19
+ def self.extended(extender)
20
+ Xolo.verbose_extend extender, self
21
+ end
22
+
23
+ # when this module is included
24
+ def self.included(includer)
25
+ Xolo.verbose_include includer, self
26
+ end
27
+
28
+ # A wrapper for JSON.parse that always uses :symbolize_names
29
+ # def self.parse_json(str)
30
+ # JSON.parse str, symbolize_names: true
31
+ # end
32
+
33
+ # A wrapper for JSON.parse that always uses :symbolize_names
34
+ # and ensures UTF-8 encoding
35
+ def parse_json(str)
36
+ JSON.parse str.force_encoding('UTF-8'), symbolize_names: true
37
+ end
38
+
39
+ end # JSON
40
+
41
+ end # Core
42
+
43
+ end # module Xolo
@@ -0,0 +1,59 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Xolo
10
+
11
+ module Core
12
+
13
+ module Loading
14
+
15
+ # touch this file to make mixins send text to stderr as things load
16
+ # or get mixed in
17
+ VERBOSE_LOADING_FILE = Pathname.new('/tmp/xolo-verbose-loading')
18
+
19
+ # Or, set this ENV var to also make mixins send text to stderr
20
+ VERBOSE_LOADING_ENV = 'XOLO_VERBOSE_LOADING'
21
+
22
+ def self.extended(extender)
23
+ Xolo.verbose_extend extender, self
24
+ end
25
+
26
+ # Only look at the filesystem once.
27
+ def verbose_loading?
28
+ return @verbose_loading unless @verbose_loading.nil?
29
+
30
+ @verbose_loading = VERBOSE_LOADING_FILE.file?
31
+ @verbose_loading ||= ENV.include? VERBOSE_LOADING_ENV
32
+ @verbose_loading
33
+ end
34
+
35
+ # Send a message to stderr if verbose loading is enabled
36
+ def load_msg(msg)
37
+ warn msg if verbose_loading?
38
+ end
39
+
40
+ # Mention that a module is being included into something
41
+ def verbose_include(includer, includee)
42
+ load_msg "--> #{includer} is including #{includee}"
43
+ end
44
+
45
+ # Mention that a module is being extended into something
46
+ def verbose_extend(extender, extendee)
47
+ load_msg "--> #{extender} is extending #{extendee}"
48
+ end
49
+
50
+ # Mention that a module is being extended into something
51
+ def verbose_inherit(child_class, parent_class)
52
+ load_msg "--> #{child_class} is a Subclass inheriting from #{parent_class}"
53
+ end
54
+
55
+ end # module
56
+
57
+ end # module
58
+
59
+ end # module Xolo
@@ -0,0 +1,292 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'io/console'
10
+
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ # Methods for formattng and sending output to stdout
16
+ # Should be included in classes as needed
17
+ #
18
+ # NOTE: Help output is auto-generated by 'optimist'
19
+ # The methods here are mostly for presenting info like
20
+ # columnizd lists and reports and the like.
21
+ module Output
22
+
23
+ # Constants
24
+ #############################
25
+ #############################
26
+
27
+ # This is used when we are not outputting to a terminal
28
+ # usually we're being piped or not running in a terminal
29
+ # so the lines should be as long as they want
30
+ DEFAULT_LINE_WIDTH = 2000
31
+
32
+ # Module methods
33
+ #
34
+ # These are available as module methods but not as 'helper'
35
+ # methods in sinatra routes & views.
36
+ #
37
+ ##############################
38
+ ##############################
39
+
40
+ # when this module is included
41
+ ##############################
42
+ def self.included(includer)
43
+ Xolo.verbose_include includer, self
44
+ end
45
+
46
+ # when this module is extended
47
+ def self.extended(extender)
48
+ Xolo.verbose_extend extender, self
49
+ end
50
+
51
+ # Instance methods
52
+ #
53
+ # These are available directly in sinatra routes and views
54
+ #
55
+ ##############################
56
+ ##############################
57
+
58
+ # @return [Integer] how many rows high is our terminal?
59
+ #########################
60
+ def terminal_height
61
+ IO.console.winsize.first
62
+ end
63
+
64
+ # @return [Integer] how many columns wide is our terminal?
65
+ #########################
66
+ def terminal_width
67
+ IO.console.winsize.last
68
+ end
69
+
70
+ # @return [Integer] how wide is our word wrap? terminal-width minus 5
71
+ #########################
72
+ def terminal_word_wrap
73
+ @terminal_word_wrap ||= terminal_width - 5
74
+ end
75
+
76
+ # format a multi-line value by prepending the desired indentation
77
+ # to all but the first line, which is expected to be indented in-place where
78
+ # its being used.
79
+ #
80
+ # @param value [String] the value to format
81
+ # @param indent [Integer] the number of spaces to indent all but the first line
82
+ # @return [String] the formatted value
83
+ #######################
84
+ def format_multiline_indent(value, indent:)
85
+ value = value.to_s
86
+ return value unless value.include? "\n"
87
+
88
+ lines = value.split("\n")
89
+ lines[1..-1].each { |line| line.prepend ' ' * indent }
90
+ lines.join("\n")
91
+ end
92
+
93
+ # TODO: Move this out of Xolo::Core
94
+ # Display a list of items in as many columns as possible
95
+ # based on terminal width, e.g. with 3 cols:
96
+ #
97
+ # a thing another thing third thing
98
+ # thing2 line2 thing third line 2
99
+ # line3 thing another one and yet a 3rd
100
+ # oh my line4 4 line ok? yes, this is it
101
+ #
102
+ # and if the list is longer than terminal height,
103
+ # pipe it through 'less'
104
+ #
105
+ # @param header [String] A string to display at the top prepended with a '#'
106
+ # and appended with a newline and a line of ######'s of the same length.
107
+ #
108
+ # @param list [Array<String>] the items to list
109
+ #
110
+ # @return [void]
111
+ ############################
112
+ def list_in_cols(header, list)
113
+ longest_list_item = list.map(&:size).max
114
+ use_columns = (longest_list_item + 5) < terminal_width
115
+
116
+ list_to_display = use_columns ? highline_cli.list(list, :columns_down) : list.join("\n")
117
+
118
+ output = +"# #{header}\n"
119
+ output << '#' * (terminal_width - 5)
120
+ output << "\n"
121
+ output << list_to_display
122
+
123
+ show_text output
124
+ end
125
+
126
+ # Generate a report of rowed/columned data, either fixed-width or tab-delimited.
127
+ #
128
+ # Title and header lines are pre-pended with '# ' for easier exclusion
129
+ # when using the report as input for some other program.
130
+ # If the :type is :fixed, so will the column header line.
131
+ # (however, for parsing this data, try using the --json option)
132
+ #
133
+ # @param lines [Array<Array>] the rows and columns of data
134
+ #
135
+ # @param type [Symbol] :fixed or :tab, defaults to :fixed
136
+ #
137
+ # @params title [String] a descriptive text or title, shown above the
138
+ # column headers. Every line is pre-pended with '# '.
139
+ # Only used on :fixed reports.
140
+ #
141
+ # @params header_row [Array<String>] the column headers. optional.
142
+ #
143
+ # @return [String] the formatted report.
144
+ #
145
+ ############################
146
+ def generate_report(lines, type: :fixed, header_row: [], title: nil)
147
+ return Xolo::BLANK if lines.pix_empty?
148
+
149
+ raise ArgumentError, 'The first argument must be an Array' unless lines.is_a?(Array)
150
+ raise ArgumentError, 'The header_row must be an Array' unless header_row.is_a? Array
151
+
152
+ # tab delim is easy
153
+ if type == :tab
154
+ report_tab = header_row.join("\t")
155
+ lines.each { |line| report_tab += "\n#{line.join("\t")}" }
156
+ return report_tab.strip
157
+ end # if :tab
158
+
159
+ # below here, fixed width
160
+
161
+ line_width, format_str = width_and_format(lines, header_row)
162
+
163
+ # title if given
164
+ report = title ? +"# #{title}\n" : +''
165
+
166
+ unless header_row.empty?
167
+ unless header_row.size == lines[0].size
168
+ raise ArgumentError,
169
+ "Header row must have #{lines[0].count} items"
170
+ end
171
+
172
+ # then the header line if given
173
+ report += format_str % header_row
174
+ # add a separator
175
+ report += '#' + ('-' * (line_width - 1)) + "\n"
176
+ end
177
+ # add the rows
178
+ lines.each { |line| report += format_str % line }
179
+
180
+ report
181
+ end # generate report
182
+
183
+ # Given an Array of Arrays representing rows and columns of data,
184
+ # figure out the appropriate line-width for the longest line
185
+ # and the printf format string to create the columns
186
+ #
187
+ #
188
+ # @param lines [Array<Array>] The rows and columns of data
189
+ #
190
+ # @param header_row [Array] An optional header row to include in the
191
+ # width calculation.
192
+ #
193
+ # @return [Array<Integer, String>] the line width and format string
194
+ #
195
+ ############################
196
+ def width_and_format(lines, header_row = [])
197
+ # below here, fixed width
198
+ format_str = +''
199
+ line_width = 0
200
+ header_row[0] = "# #{header_row[0]}"
201
+
202
+ col_widths(lines, header_row).each do |w|
203
+ # make sure there's a space between columns
204
+ col_width = w + 1
205
+
206
+ # add the column to the printf format
207
+ format_str += "%-#{col_width}s"
208
+ line_width += col_width
209
+ end
210
+ format_str += "\n"
211
+
212
+ # if needed, limit the total line width for the header the width of the terminal
213
+ max_width = $stdout.tty? ? terminal_word_wrap : DEFAULT_LINE_WIDTH
214
+ line_width = max_width if line_width > max_width
215
+
216
+ [line_width, format_str]
217
+ end
218
+
219
+ # Given an Array of Arrays representing rows and columns of data
220
+ # figure out the widest width of each column and return an array
221
+ # of integers representing those widths
222
+ #
223
+ # @param data [Array<Array>] The rows and columns of data
224
+ #
225
+ # @param header_row [Array] An optional header row to include in the
226
+ # width calculation.
227
+ #
228
+ # @return [Array<Integer>] the max widths of each column of data.
229
+ #
230
+ ############################
231
+ def col_widths(data, header_row = [])
232
+ widths = header_row.map { |c| c.to_s.length }
233
+ data.each do |row|
234
+ row.each_index do |col|
235
+ this_width = row[col].to_s.length
236
+ widths[col] = this_width if this_width > widths[col].to_i
237
+ end # do field
238
+ end # do line
239
+ widths
240
+ end
241
+
242
+ # Should a given string be displayed via /usr/bin/less?
243
+ # true if stdout is a tty AND the string is > (terminal height - 2)
244
+ # The - 2 accounts for the final newline and an extra line at the
245
+ # bottom of the terminal, for better visual results
246
+ #########################
247
+ def use_less?(text)
248
+ $stdout.tty? && text.lines.size > (terminal_height - 2)
249
+ end
250
+
251
+ # Send a string to the terminal, possibly piping it through 'less'
252
+ # if the number of lines is greater than the number of terminal lines
253
+ #
254
+ # @param text[String] the text to send to the terminal
255
+ #
256
+ # @param show_help[Boolean] should the text have a line at the top
257
+ # showing basic 'less' key commands.
258
+ #
259
+ # @result [void]
260
+ #
261
+ ############################
262
+ def show_text(text, show_help = true)
263
+ unless use_less?(text)
264
+ puts text
265
+ return
266
+ end
267
+
268
+ if show_help
269
+ help = "# -- Using /usr/bin/less: ' ' next, 'b' prev, 'q' exit, 'h' help --"
270
+ text = "#{help}\n#{text}"
271
+ end
272
+
273
+ # point stdout through less, print, then restore stdout
274
+ less = IO.popen('/usr/bin/less', 'w')
275
+
276
+ begin
277
+ less.puts text
278
+
279
+ # this catches the quitting of 'less' before all the output
280
+ # is displayed
281
+ rescue Errno::EPIPE
282
+ true
283
+ ensure
284
+ less&.close
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+ end
291
+
292
+ end # module
@@ -0,0 +1,21 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # main module
9
+ module Xolo
10
+
11
+ module Core
12
+
13
+ module Version
14
+
15
+ VERSION = '1.0.1'.freeze
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end # module
data/lib/xolo/core.rb ADDED
@@ -0,0 +1,46 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ # Load the core xolo functionality
10
+ #
11
+ # This is done automatically when you `require 'xolo/admin'` or
12
+ # `require 'xolo/server'`
13
+
14
+ # Ruby Standard Libraries
15
+ ######
16
+ require 'English'
17
+ require 'date'
18
+ require 'time'
19
+ require 'pathname'
20
+ require 'json'
21
+
22
+ # Other Gems to include at this level
23
+ require 'pixar-ruby-extensions'
24
+
25
+ # Internal requires - order matters
26
+ require 'xolo/core/loading'
27
+ require 'xolo/core/version'
28
+ require 'xolo/core/constants'
29
+ require 'xolo/core/exceptions'
30
+
31
+ # The main module
32
+ module Xolo
33
+
34
+ extend Xolo::Core::Loading
35
+ include Xolo::Core::Version
36
+ include Xolo::Core::Constants
37
+ include Xolo::Core::Exceptions
38
+
39
+ end # module Xolo
40
+
41
+ require 'xolo/core/json_wrappers'
42
+ require 'xolo/core/base_classes/configuration'
43
+ require 'xolo/core/base_classes/server_object'
44
+ require 'xolo/core/base_classes/title'
45
+ require 'xolo/core/base_classes/version'
46
+ require 'xolo/core/output'
@@ -395,7 +395,6 @@ module Xolo
395
395
  # @!attribute jamf_gui_hostname
396
396
  # @return [String] The hostname of the Jamf Pro server used for links to the GUI webapp
397
397
  jamf_gui_hostname: {
398
- required: true,
399
398
  type: :string,
400
399
  desc: <<~ENDDESC
401
400
  The hostname of the Jamf Pro server used for links to the GUI webapp, if different from the jamf_hostname.
@@ -478,7 +477,7 @@ module Xolo
478
477
  desc: <<~ENDDESC
479
478
  The password for the username that connects to the Jamf Pro APIs.
480
479
 
481
- If you start this value with a vertical bar '|', everything after the bar is a command to be executed by the server at start-time. The command must return the certificate to standard output. This is useful when using a secret-storage system to manage secrets.
480
+ If you start this value with a vertical bar '|', everything after the bar is a command to be executed by the server at start-time. The command must return the password to standard output. This is useful when using a secret-storage system to manage secrets.
482
481
 
483
482
  If the value is a path to a readable file, the file's contents are used.
484
483
 
@@ -68,6 +68,7 @@ module Xolo
68
68
 
69
69
  host = Xolo::Server.config.jamf_gui_hostname
70
70
  host ||= Xolo::Server.config.jamf_hostname
71
+
71
72
  port = Xolo::Server.config.jamf_gui_port
72
73
  port ||= Xolo::Server.config.jamf_port
73
74
 
@@ -346,21 +346,16 @@ module Xolo
346
346
  # @return [void]
347
347
  ################################
348
348
  def configure_jamf_normal_ea
349
+ # nothing to do if its nil, if we need to delete it, that'll happen later
350
+ return if version_script_contents.pix_empty?
351
+
349
352
  progress "Jamf: Configuring regular extension attribute '#{jamf_normal_ea_name}'", log: :info
350
353
 
351
354
  jamf_normal_ea.description = "The version of xolo title '#{title}' installed on the machine"
352
- jamf_normal_ea.data_type = :string
353
-
354
- # this is our incoming or already-existing EA script
355
- if version_script_contents.pix_empty?
356
- # nothing to do if its nil, if we need to delete it, that'll happen later
357
- else
358
- jamf_normal_ea.enabled = true
359
- jamf_normal_ea.input_type = 'script'
360
- jamf_normal_ea.script = version_script_contents
361
- end
362
-
363
- jamf_normal_ea.script = scr
355
+ jamf_normal_ea.data_type = 'String'
356
+ jamf_normal_ea.input_type = 'script'
357
+ jamf_normal_ea.enable
358
+ jamf_normal_ea.script = version_script_contents
364
359
  jamf_normal_ea.save
365
360
  end
366
361
 
@@ -86,8 +86,10 @@ module Xolo
86
86
  sleep 2
87
87
 
88
88
  # re-fetch the title from ted and enable it
89
+ progress "Title Editor: Enabling SoftwareTitle '#{title}'", log: :info
89
90
  ted_title(refresh: true).enable
90
91
 
92
+ # cache the new title object id
91
93
  self.ted_id_number = ted_title.softwareTitleId
92
94
  end
93
95
 
@@ -197,6 +199,9 @@ module Xolo
197
199
  # This will also apply the changes to all patch component criteria
198
200
  apply_requirement_changes
199
201
 
202
+ # re-enable all patches, after any change, which might have disabled them
203
+ reenable_all_ted_patches
204
+
200
205
  # mucking with the patches often disables the title, make sure its enabled.
201
206
  enable_ted_title
202
207
 
@@ -416,6 +421,8 @@ module Xolo
416
421
  #
417
422
  # Re-enable the title in ted after updating any patches
418
423
  #
424
+ # TODO: Now that we are using stub patches, do we need the loop?
425
+ #
419
426
  # @return [void]
420
427
  ##############################
421
428
  def enable_ted_title
@@ -430,10 +437,10 @@ module Xolo
430
437
  loop do
431
438
  raise Xolo::TimeoutError, "Title Editor: Timed out waiting for SoftwareTitle '#{title}' to enable" if Time.now > breaktime
432
439
 
433
- sleep 5
434
440
  ted_title(refresh: true).enable
435
441
  break
436
442
  rescue Windoo::MissingDataError => e
443
+ sleep 5
437
444
  log_debug "Title Editor: Looping up to #{Xolo::Server::Constants::MAX_JAMF_WAIT_FOR_TITLE_EDITOR} secs while re-enabling SoftwareTitle '#{title}': #{e}"
438
445
 
439
446
  # make sure all patches are enabled, even tho at least one should have been
@@ -500,16 +507,25 @@ module Xolo
500
507
  if ted_title.patches.empty?
501
508
  create_and_enable_stub_patch_in_ted(ted_title)
502
509
  else
503
- # make sure at least one patch is enabled
504
- unless ted_title.patches.to_a.any?(&:enabled?)
505
- progress "Title Editor: Enabling at least the first patch for title '#{title}'", log: :info
506
- ted_title.patches.first.enable
510
+ # repair all patches, because reparing the title might have changed the requirements
511
+ progress "Title Editor: Re-enabling all patches for '#{title}'", log: :info
512
+ version_objects.each do |vobj|
513
+ vobj.repair_ted_patch
507
514
  end
508
515
  end
509
516
 
510
517
  ted_title.enable unless ted_title(refresh: true).enabled?
511
518
  end
512
519
 
520
+ # Re-enable all patches in the title editor
521
+ # @return [void]
522
+ ###########################
523
+ def reenable_all_ted_patches
524
+ # re-enable all patches, after any change, which might have disabled them
525
+ progress "Title Editor: Re-enabling all patches for '#{title}'", log: :info
526
+ version_objects.each { |vobj| vobj.enable_ted_patch }
527
+ end
528
+
513
529
  end # TitleEditorTitle
514
530
 
515
531
  end # Mixins