xolo-admin 1.0.1 → 2.0.2

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.
@@ -39,7 +39,13 @@ module Xolo
39
39
  XOLO_CREDS_SVC = 'com.pixar.xolo.password'
40
40
 
41
41
  # the Label for the generic 'Xolo::Admin::Credentials' keychain entry
42
- XOLO_CREDS_LBL = '"Xolo Admin Password"'
42
+ XOLO_CREDS_LBL = 'Xolo Admin Password'
43
+
44
+ # Pre v2.0.0, the label was different. We need to check for the old one when fetching the password
45
+ # and if it's there, update it to the new one.
46
+ # TODO: Remove this in 2.1.0 or other appropriate future version,
47
+ # once we can be sure most users have updated to 2.0.0 or later.
48
+ XOLO_CREDS_LBL_OLD = '"Xolo Admin Password"'
43
49
 
44
50
  # Module methods
45
51
  ##############################
@@ -70,10 +76,16 @@ module Xolo
70
76
  cmd << '-w'
71
77
  run_security(cmd.map { |i| security_escape i }.join(' '))
72
78
 
79
+ # TEMPORARY - check for the old label if we didn't find the password with the new label, and if we find it with the old label, update it to the new label
80
+ # TODO: Remove this in 2.1.0 or other appropriate future version,
81
+ # once we can be sure most users have updated to 2.0.0 or later.
82
+ rescue Xolo::NoSuchItemError
83
+ fetch_and_update_pw_if_necessary
84
+
73
85
  # If we can't access the keychain, prompt for the password. This is usually
74
86
  # when we're running in a non-GUI session, e.g. via ssh.
75
87
  rescue Xolo::KeychainError
76
- raise unless @security_exit_status.exitstatus == SEC_STATUS_NO_GUI_ERROR
88
+ raise unless @security_exit_status.exitstatus == Xolo::Core::SecurityCmd::SEC_STATUS_NO_GUI_ERROR
77
89
  raise unless STDOUT.isatty
78
90
 
79
91
  question = "Keychain not accessible.\nPlease enter the xolo admin password for #{config.admin}: "
@@ -82,6 +94,38 @@ module Xolo
82
94
  end
83
95
  end
84
96
 
97
+ # TEMPORARY - update the label of the existing keychain item if it has the old label but not the new one,
98
+ # to avoid prompting users to re-enter their password when we change the label in 2.0.0
99
+ # TODO: Remove this in 2.1.0 or other appropriate future version,
100
+ # once we can be sure most users have updated to 2.0.0 or later.
101
+ #####################
102
+ def fetch_and_update_pw_if_necessary
103
+ cmd = ['find-generic-password']
104
+ cmd << '-s'
105
+ cmd << XOLO_CREDS_SVC
106
+ cmd << '-l'
107
+ cmd << XOLO_CREDS_LBL_OLD
108
+ cmd << '-w'
109
+ pw = run_security(cmd.map { |i| security_escape i }.join(' '))
110
+
111
+ if @security_exit_status.exitstatus == Xolo::Core::SecurityCmd::SEC_STATUS_NOT_FOUND_ERROR
112
+ raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
113
+ end
114
+
115
+ # then delete the old item with the old label
116
+ cmd = ['delete-generic-password']
117
+ cmd << '-s'
118
+ cmd << XOLO_CREDS_SVC
119
+ cmd << '-l'
120
+ cmd << XOLO_CREDS_LBL_OLD
121
+ run_security(cmd.map { |i| security_escape i }.join(' '))
122
+
123
+ # if we found the password with the old label, update it to the new label
124
+ store_pw(ENV['USER'], pw)
125
+
126
+ pw
127
+ end
128
+
85
129
  # Store an item in the default keychain
86
130
  #
87
131
  # @param acct [String] The username for the password.
@@ -141,50 +185,50 @@ module Xolo
141
185
  # @return [String] the stdout of the 'security' command.
142
186
  #
143
187
  ######
144
- def run_security(cmd)
145
- output = Xolo::BLANK
146
- errs = Xolo::BLANK
147
-
148
- Open3.popen3("#{SEC_COMMAND} -i") do |stdin, stdout, stderr, wait_thr|
149
- # pid = wait_thr.pid # pid of the started process.
150
- stdin.puts cmd
151
- stdin.close
152
-
153
- output = stdout.read
154
- errs = stderr.read
155
-
156
- @security_exit_status = wait_thr.value # Process::Status object returned.
157
- end
158
- # exit 44 is 'The specified item could not be found in the keychain'
159
- return output.chomp if @security_exit_status.success?
160
-
161
- case @security_exit_status.exitstatus
162
- when SEC_STATUS_AUTH_ERROR
163
- raise Xolo::KeychainError, 'Problem accessing login keychain. Is it locked?'
164
-
165
- when SEC_STATUS_NOT_FOUND_ERROR
166
- raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
167
-
168
- else
169
- errs.chomp!
170
- errs =~ /: returned\s+(-?\d+)$/
171
- errnum = Regexp.last_match(1)
172
- desc = errnum ? security_error_desc(errnum) : errs
173
- desc ||= errs
174
- raise Xolo::KeychainError, "#{desc.gsub("\n", '; ')}; exit status #{@security_exit_status.exitstatus}"
175
- end # case
176
- end # run_security
188
+ # def run_security(cmd)
189
+ # output = Xolo::BLANK
190
+ # errs = Xolo::BLANK
191
+
192
+ # Open3.popen3("#{Xolo::Core::SecurityCmd::SEC_COMMAND} -i") do |stdin, stdout, stderr, wait_thr|
193
+ # # pid = wait_thr.pid # pid of the started process.
194
+ # stdin.puts cmd
195
+ # stdin.close
196
+
197
+ # output = stdout.read
198
+ # errs = stderr.read
199
+
200
+ # @security_exit_status = wait_thr.value # Process::Status object returned.
201
+ # end
202
+ # # exit 44 is 'The specified item could not be found in the keychain'
203
+ # return output.chomp if @security_exit_status.success?
204
+
205
+ # case @security_exit_status.exitstatus
206
+ # when Xolo::Core::SecurityCmd::SEC_STATUS_AUTH_ERROR
207
+ # raise Xolo::KeychainError, 'Problem accessing login keychain. Is it locked?'
208
+
209
+ # when Xolo::Core::SecurityCmd::SEC_STATUS_NOT_FOUND_ERROR
210
+ # raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
211
+
212
+ # else
213
+ # errs.chomp!
214
+ # errs =~ /: returned\s+(-?\d+)$/
215
+ # errnum = Regexp.last_match(1)
216
+ # desc = errnum ? security_error_desc(errnum) : errs
217
+ # desc ||= errs
218
+ # raise Xolo::KeychainError, "#{desc.gsub("\n", '; ')}; exit status #{@security_exit_status.exitstatus}"
219
+ # end # case
220
+ # end # run_security
177
221
 
178
222
  # use `security error` to get a description of an error number
179
223
  ##############
180
- def security_error_desc(num)
181
- desc = `#{SEC_COMMAND} error #{num}`
182
- return if desc.include?('unknown error')
224
+ # def security_error_desc(num)
225
+ # desc = `#{Xolo::Core::SecurityCmd::SEC_COMMAND} error #{num}`
226
+ # return if desc.include?('unknown error')
183
227
 
184
- desc.chomp.split(num).last
185
- rescue StandardError
186
- nil
187
- end
228
+ # desc.chomp.split(num).last
229
+ # rescue StandardError
230
+ # nil
231
+ # end
188
232
 
189
233
  # given a string, wrap it in single quotes and escape internal single quotes
190
234
  # and backslashes so it can be used in the interactive 'security' command
@@ -193,17 +237,17 @@ module Xolo
193
237
  #
194
238
  # @return [String] the escaped string
195
239
  ###################
196
- def security_escape(str)
197
- # first escape backslashes
198
- str = str.to_s.gsub '\\', '\\\\\\'
240
+ # def security_escape(str)
241
+ # # first escape backslashes
242
+ # str = str.to_s.gsub '\\', '\\\\\\'
199
243
 
200
- # then single quotes
201
- str.gsub! "'", "\\\\'"
244
+ # # then single quotes
245
+ # str.gsub! "'", "\\\\'"
202
246
 
203
- # if other things need escaping, add them here
247
+ # # if other things need escaping, add them here
204
248
 
205
- "'#{str}'"
206
- end # security_escape
249
+ # "'#{str}'"
250
+ # end # security_escape
207
251
 
208
252
  end # module Prefs
209
253
 
@@ -1,30 +1,22 @@
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
1
  # frozen_string_literal: true
9
2
 
10
- # MonkeyPatch HighLine::Terminal#readline_read so that Readline
11
- # lines can be case-insensitive, and have a prompt.
12
- #
13
- # To use a prompt, put it in the environtment variable 'XADM_HIGHLINE_READLINE_PROMPT'
14
- #
15
- # To make the readline completion case-insensitive, set the environment
16
- # variable XADM_HIGHLINE_READLINE_CASE_INSENSITIVE to anything.
17
- #
18
- # This really only modifies the Regexp used for the completion_proc to make it
19
- # case insensitive if desired (adding an 'i' to the end)
20
- # and sets the prompt when calling Readline.readline
21
- #
22
- # TODO: Do this 'smartly' as with the monkeypatches in pixar-ruby-extensions
23
- #
24
3
  class HighLine
25
4
 
26
5
  class Terminal
27
6
 
7
+ # MonkeyPatch HighLine::Terminal#readline_read so that Readline
8
+ # lines can be case-insensitive, and have a prompt.
9
+ #
10
+ # To use a prompt, put it in the environtment variable 'XADM_HIGHLINE_READLINE_PROMPT'
11
+ #
12
+ # To make the readline completion case-insensitive, set the environment
13
+ # variable XADM_HIGHLINE_READLINE_CASE_INSENSITIVE to anything.
14
+ #
15
+ # This really only modifies the Regexp used for the completion_proc to make it
16
+ # case insensitive if desired (adding an 'i' to the end)
17
+ # and sets the prompt when calling Readline.readline
18
+ #################################
19
+ #
28
20
  # Use readline to read one line
29
21
  # @param question [HighLine::Question] question from where to get
30
22
  # autocomplete candidate strings
@@ -51,31 +43,6 @@ class HighLine
51
43
  raw_answer
52
44
  end
53
45
 
54
- # Get one line from terminal using default #gets method.
55
- ##############################
56
- # def get_line_default(highline)
57
- # raise EOFError, 'The input stream is exhausted.' if highline.track_eof? && highline.input.eof?
58
-
59
- # highline.output.print "#{ENV['XADM_HIGHLINE_LINE_PROMPT']}" if ENV['XADM_HIGHLINE_LINE_PROMPT']
60
-
61
- # highline.input.gets
62
- # end
63
-
64
- end # terminal
65
-
66
- # # Deals with the task of "asking" a question
67
- # class QuestionAsker
68
-
69
- # alias ask_once_real ask_once
70
-
71
- # # Gets just one answer, as opposed to #gather_answers
72
- # #
73
- # # @return [String] answer
74
- # def ask_once
75
- # @highline.output.print "#{ENV['XADM_HIGHLINE_LINE_PROMPT']}" if ENV['XADM_HIGHLINE_LINE_PROMPT']
76
- # ask_once_real
77
- # end
78
-
79
- # end # question asker
46
+ end # class Terminal
80
47
 
81
48
  end
@@ -98,17 +98,34 @@ module Xolo
98
98
 
99
99
  # The menu items for setting values
100
100
  cmd_details(cmd)[:opts].each do |key, deets|
101
+ # dont displau items for the other kind of action
102
+ next if deets[:add_only] && edit_command?
103
+ next if deets[:edit_only] && add_command?
104
+
105
+ # only show items for the current title type, if applicable
106
+ if deets[:title_type]
107
+ if walkthru_cmd_opts[:subscribed]
108
+ next if deets[:title_type] == Xolo::MANAGED
109
+ elsif deets[:title_type] == Xolo::SUBSCRIBED
110
+ next
111
+ end
112
+ end
113
+
101
114
  curr_val = current_opt_values[key]
102
- new_val = walkthru_cmd_opts[key]
115
+
103
116
  not_avail = send(deets[:walkthru_na]) if deets[:walkthru_na]
117
+
118
+ # if a value is not available, remove any previously set value
119
+ walkthru_cmd_opts.delete_field(key) if not_avail && walkthru_cmd_opts.to_h.key?(key)
120
+
121
+ new_val = walkthru_cmd_opts[key]
122
+
104
123
  menu_item = menu_item_text(deets[:label], oldval: curr_val, newval: new_val, not_avail: not_avail)
105
124
 
106
125
  # no processing if item not available
107
126
  if not_avail
108
- # menu.choice(nil, nil, menu_item) {}
109
127
  menu.choice(menu_item) {}
110
128
  else
111
- # menu.choice(nil, nil, menu_item) { prompt_for_walkthru_value key, deets, curr_val }
112
129
  menu.choice(menu_item) { prompt_for_walkthru_value key, deets, curr_val }
113
130
  end
114
131
  end
@@ -139,22 +156,63 @@ module Xolo
139
156
  end # until done with menu
140
157
  end # def self.display_title_menu(_title)
141
158
 
159
+ # @return [String, nil] If a string, a reason why the given menu item is not available now.
160
+ # If nil, the menu item is displayed normally.
161
+ ##############################
162
+ def display_name_na
163
+ return unless walkthru_cmd_opts[:subscribed]
164
+
165
+ 'N/A when subcribing via a Patch Source'
166
+ end
167
+
168
+ # @return [String, nil] If a string, a reason why the given menu item is not available now.
169
+ # If nil, the menu item is displayed normally.
170
+ ##############################
171
+ def publisher_na
172
+ return unless walkthru_cmd_opts[:subscribed]
173
+
174
+ 'N/A when subcribing via a Patch Source'
175
+ end
176
+
142
177
  # @return [String, nil] If a string, a reason why the given menu item is not available now.
143
178
  # If nil, the menu item is displayed normally.
144
179
  ##############################
145
180
  def version_script_na
146
- return unless walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id]
181
+ unless walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id] || walkthru_cmd_opts[:subscribed]
182
+ return
183
+ end
147
184
 
148
- 'N/A when using App Name/BundleID'
185
+ 'N/A when using App Name/BundleID, or subcribing via a Patch Source'
149
186
  end
150
187
 
151
188
  # @return [String, nil] If a string, a reason why the given menu item is not available now.
152
189
  # If nil, the menu item is displayed normally.
153
190
  ##############################
154
191
  def app_name_bundleid_na
155
- return unless walkthru_cmd_opts[:version_script]
192
+ return unless walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:subscribed]
193
+
194
+ 'N/A when using Version Script, or subcribing via a Patch Source'
195
+ end
196
+
197
+ # @return [String, nil] If a string, a reason why the given menu item is not available now.
198
+ # If nil, the menu item is displayed normally.
199
+ ##############################
200
+ def patch_source_na
201
+ # if walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id]
202
+ # 'N/A when using Version Script or App Name/BundleID'
203
+ # elsif walkthru_cmd_opts[:publisher] || walkthru_cmd_opts[:display_name]
204
+ # 'N/A when using Display Name or Publisher'
205
+ # end
206
+ nil unless walkthru_cmd_opts[:subscribed]
207
+ end
208
+
209
+ # @return [String, nil] If a string, a reason why the given menu item is not available now.
210
+ # If nil, the menu item is displayed normally.
211
+ ##############################
212
+ def title_id_na
213
+ return if walkthru_cmd_opts[:patch_source]
156
214
 
157
- 'N/A when using Version Script'
215
+ 'N/A until Patch Source is set'
158
216
  end
159
217
 
160
218
  # @return [String, nil] If a string, a reason why the given menu item is not available now.
@@ -223,7 +281,14 @@ module Xolo
223
281
 
224
282
  header_text.sub! Xolo::Admin::Options::TARGET_TITLE_PLACEHOLDER, cli_cmd.title if cli_cmd.title
225
283
  header_text.sub! Xolo::Admin::Options::TARGET_VERSION_PLACEHOLDER, cli_cmd.version if cli_cmd.version
226
-
284
+ if edit_command?
285
+ header_text <<
286
+ if current_opt_values[:subscribed]
287
+ " (subscribed as '#{current_opt_values[:display_name]}')"
288
+ else
289
+ ' (managed)'
290
+ end
291
+ end
227
292
  header_sep_line = Xolo::DASH * header_text.length
228
293
 
229
294
  system 'clear'
@@ -231,7 +296,7 @@ module Xolo
231
296
  #{header_sep_line}
232
297
  #{header_text}
233
298
  #{header_sep_line}
234
- Current Settings => New Settings
299
+ Current Settings -> New Settings
235
300
 
236
301
  ENDPUTS
237
302
  end
@@ -455,7 +520,11 @@ module Xolo
455
520
  validated_new_val = deets[:multi] ? [] : nil
456
521
  all_done = false
457
522
  until all_done
458
- latest_input = Readline.readline(prompt, true)
523
+ latest_input = Readline.readline(prompt, true).strip
524
+ # dragging in items from the finder will esacpe spaces in the path with \'s
525
+ # in the shell this is good, but ruby is interpreting the \'s, so lets remove them.
526
+ latest_input.gsub!(/\\ /, ' ')
527
+
459
528
  break if latest_input == Xolo::X
460
529
  return Xolo::NONE if !deets[:required] && (latest_input == Xolo::NONE)
461
530
 
@@ -503,8 +572,8 @@ module Xolo
503
572
  # if deets[:readline] is a symbol, its an xadm method that returns an array
504
573
  # of the possible values for readline completion and validation;
505
574
  # only things in the array are allowed, so no need for other validation or conversion
506
- # We add 'x' and 'none' to the list so they will be accepted for exiting and
507
- # clearing.
575
+ # We add 'x' and 'none' to the list for clearing it, and if a multi-value we add 'x'
576
+ # for exiting out of the prompt
508
577
  #
509
578
  # if its just truthy then we use readline without a pre-set list of values
510
579
  # (e.g. paths, which might not exist locally) and may have a separate validate
@@ -513,10 +582,12 @@ module Xolo
513
582
  if deets[:readline]
514
583
  if deets[:readline].is_a? Symbol
515
584
  convert = send deets[:readline]
516
- convert << Xolo::NONE unless deets[:required]
517
- convert << Xolo::X
585
+ convert << Xolo::NONE unless convert.include?(Xolo::NONE) || deets[:required]
586
+ convert << Xolo::X if deets[:multi] && !convert.include?(Xolo::X)
518
587
  # if we're doing release groups, make sure the list includes 'all',
519
- convert << Xolo::TARGET_ALL if deets[:label] == Xolo::Admin::Title::ATTRIBUTES[:release_groups][:label]
588
+ if !convert.include?(Xolo::TARGET_ALL) && deets[:label] == Xolo::Admin::Title::ATTRIBUTES[:release_groups][:label]
589
+ convert << Xolo::TARGET_ALL
590
+ end
520
591
  validate = nil
521
592
  end
522
593
  true
@@ -722,9 +793,67 @@ module Xolo
722
793
 
723
794
  missing_values << deets[:label]
724
795
  end
796
+
797
+ missing_values += title_missing_values if title_command?
798
+
725
799
  missing_values
726
800
  end
727
801
 
802
+ # Process missing valus for titles
803
+ # @return [Array<String>] The title-specific missing values
804
+ ##################################
805
+ def title_missing_values
806
+ title_missing_values = []
807
+
808
+ # if subscribing, need patch source and title id
809
+ # if walkthru_cmd_opts[:type] == Xolo::SUBSCRIBED
810
+ if walkthru_cmd_opts[:subscribed]
811
+ unless walkthru_cmd_opts[:patch_source]
812
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:patch_source][:label]
813
+ end
814
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:title_id][:label] unless walkthru_cmd_opts[:title_id]
815
+
816
+ # if managed, need display name, publisher and version script or app name/bundle id,
817
+ else
818
+ unless walkthru_cmd_opts[:publisher]
819
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:publisher][:label]
820
+ end
821
+ unless walkthru_cmd_opts[:display_name]
822
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:display_name][:label]
823
+ end
824
+
825
+ # version script or app name/bundle id
826
+ if walkthru_cmd_opts[:version_script] || walkthru_cmd_opts[:app_name] || walkthru_cmd_opts[:app_bundle_id]
827
+ # need both app name and bundle id if using either
828
+ if walkthru_cmd_opts[:app_name] && !walkthru_cmd_opts[:app_bundle_id]
829
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:app_bundle_id][:label]
830
+ elsif walkthru_cmd_opts[:app_bundle_id] && !walkthru_cmd_opts[:app_name]
831
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:app_name][:label]
832
+ end
833
+ else
834
+ title_missing_values << "#{Xolo::Admin::Title::ATTRIBUTES[:version_script][:label]} OR #{Xolo::Admin::Title::ATTRIBUTES[:app_name][:label]} & #{Xolo::Admin::Title::ATTRIBUTES[:app_bundle_id][:label]}"
835
+ end
836
+
837
+ end
838
+
839
+ # if expiring, need expire path
840
+ if walkthru_cmd_opts[:expiration].to_i.positive? && !walkthru_cmd_opts[:expiration_paths].pix_empty?
841
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:expiration_paths][:label]
842
+ end
843
+
844
+ # if in ssvc, need category and icon
845
+ return title_missing_values unless walkthru_cmd_opts[:self_service]
846
+
847
+ unless walkthru_cmd_opts[:self_service_category]
848
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:self_service_category][:label]
849
+ end
850
+ return title_missing_values if walkthru_cmd_opts[:self_service_icon]
851
+
852
+ title_missing_values << Xolo::Admin::Title::ATTRIBUTES[:self_service_icon][:label]
853
+
854
+ title_missing_values
855
+ end # def title_missing_values
856
+
728
857
  # Prompt for an editor to use from those in MULTILINE_EDITORS
729
858
  # @return [String] the path to an editor to use for multiline values.
730
859
  ##################
@@ -32,6 +32,9 @@ module Xolo
32
32
  # Xolo server route to the list available categories
33
33
  CATEGORY_NAME_ROUTE = "#{JAMF_ROUTE_BASE}/category-names"
34
34
 
35
+ # Xolo server route to the list of available titles for subscription
36
+ AVAILABLE_TITLES_ROUTE = "#{JAMF_ROUTE_BASE}/available-titles-for-subscription"
37
+
35
38
  # Module Methods
36
39
  ##########################
37
40
  ##########################
@@ -68,6 +71,17 @@ module Xolo
68
71
  @jamf_category_names ||= server_cnx.get(CATEGORY_NAME_ROUTE).body
69
72
  end
70
73
 
74
+ # @return [Array<String>] data about all titles available for subscription in Jamf Pro.
75
+ #######################
76
+ def jamf_available_titles
77
+ @jamf_available_titles ||= server_cnx.get(AVAILABLE_TITLES_ROUTE).body
78
+ end
79
+
80
+ # @return [Array<String>] All Patch Sources with any available titles in Jamf Pro
81
+ def jamf_patch_sources_with_available_titles
82
+ @jamf_patch_sources_with_available_titles ||= jamf_available_titles.map { |t| t[:source_name] }.sort.uniq
83
+ end
84
+
71
85
  end # module
72
86
 
73
87
  end # module Admin
@@ -152,6 +152,7 @@ module Xolo
152
152
 
153
153
  LIST_TITLES_CMD = 'list-titles'
154
154
  ADD_TITLE_CMD = 'add-title'
155
+ SUBSCRIBE_CMD = 'subscribe'
155
156
  EDIT_TITLE_CMD = 'edit-title'
156
157
  DELETE_TITLE_CMD = 'delete-title'
157
158
  FREEZE_TITLE_CMD = 'freeze'
@@ -174,6 +175,7 @@ module Xolo
174
175
 
175
176
  LIST_GROUPS_CMD = 'list-groups'
176
177
  LIST_CATEGORIES_CMD = 'list-categories'
178
+ LIST_AVAILABLE_CMD = 'list-available'
177
179
  SAVE_CLIENT_CODE_CMD = 'save-client'
178
180
 
179
181
  SERVER_STATUS_CMD = 'server-status'
@@ -190,7 +192,7 @@ module Xolo
190
192
 
191
193
  HELP_OPT = '--help'
192
194
 
193
- DFT_CMD_TITLE_ARG_BANNER = " title: The unique name of a title in Xolo, e.g. 'google-chrome'"
195
+ DFT_CMD_TITLE_ARG_BANNER = " title: The unique name of a title in Xolo, e.g. 'google-chrome'\n Must be lowercase alphanumeric and dashes only."
194
196
  DFT_CMD_VERSION_ARG_BANNER = " version: The version of the title you are working with. e.g. '12.34.5'"
195
197
 
196
198
  TARGET_TITLE_PLACEHOLDER = Xolo::Admin::Title::TARGET_TITLE_PLACEHOLDER
@@ -299,7 +301,7 @@ module Xolo
299
301
  readline_prompt: 'Group Name',
300
302
  readline: :jamf_computer_group_names,
301
303
  desc: <<~ENDDESC
302
- One or more Jamf Computer Group names or ids whose members will receive the MDM deployment.
304
+ One or more Jamf Computer Group names whose members will receive the MDM deployment.
303
305
 
304
306
  When using the --groups CLI option, you can specify more than one group by using the option more than once, or by providing a single option value with the groups separated by commas.
305
307
  ENDDESC
@@ -425,6 +427,17 @@ module Xolo
425
427
  confirmation: true
426
428
  },
427
429
 
430
+ # SUBSCRIBE_CMD => {
431
+ # desc: 'Subscribe to a software title from a Patch Source defined in Jamf Pro',
432
+ # display: "#{SUBSCRIBE_CMD} title",
433
+ # opts: Xolo::Admin::Title.subscribe_cli_opts,
434
+ # walkthru_header: "Subscribing to Xolo Title '#{TARGET_TITLE_PLACEHOLDER}'",
435
+ # target: :title,
436
+ # process_method: :subscribe_title,
437
+ # streamed_response: true,
438
+ # confirmation: true
439
+ # },
440
+
428
441
  EDIT_TITLE_CMD => {
429
442
  desc: 'Edit an exising software title',
430
443
  display: "#{EDIT_TITLE_CMD} title",
@@ -554,7 +567,7 @@ module Xolo
554
567
  before the MDM command is sent.
555
568
 
556
569
  Computers can be specified by name, serial number, or Jamf ID. Groups can be specified by
557
- name or ID.
570
+ name.
558
571
 
559
572
  NOTE: Any automated installs (via Pilot or Release groups) will happen eventually anyway.
560
573
  All Macs with the title already installed will also get new versions automatically.
@@ -688,6 +701,21 @@ module Xolo
688
701
  process_method: :list_categories
689
702
  },
690
703
 
704
+ LIST_AVAILABLE_CMD => {
705
+ desc: 'List all titles available for subscription, and their Patch Sources and Title IDs.',
706
+ long_desc: <<~ENDLONG,
707
+ When adding a subscribed title to Xolo, you need to know the Patch Source and Title ID.
708
+ This command lists all available titles from all defined Patch Sources on the Jamf Pro server,
709
+ and the unique identifiers for each. You can use those values with the --patch-source and --title-id
710
+ options of the 'xadm add-title' command.
711
+ ENDLONG
712
+ display: LIST_AVAILABLE_CMD,
713
+ usage: "#{Xolo::Admin::EXECUTABLE_FILENAME} [global-options] #{LIST_AVAILABLE_CMD}",
714
+ opts: {},
715
+ arg_banner: :none,
716
+ process_method: :list_available_titles
717
+ },
718
+
691
719
  SAVE_CLIENT_CODE_CMD => {
692
720
  desc: 'Save the xolo client tool to a directory for packaging and deployment.',
693
721
  long_desc: <<~ENDLONG,
@@ -1090,6 +1118,12 @@ module Xolo
1090
1118
  # versions
1091
1119
  elsif version_command?
1092
1120
 
1121
+ # make note of the title being subscribed or managed
1122
+ title_obj = Xolo::Admin::Title.fetch(cli_cmd.title, server_cnx)
1123
+
1124
+ @current_opt_values[:subscribed] = title_obj.subscribed?
1125
+ @current_opt_values[:display_name] = title_obj.display_name
1126
+
1093
1127
  # adding a new one?
1094
1128
  if add_command?
1095
1129
  prev_version = Xolo::Admin::Title.latest_version cli_cmd.title, server_cnx