xolo-admin 1.0.0 → 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.
@@ -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
@@ -26,7 +26,7 @@ module Xolo
26
26
  # Routes for server admins
27
27
 
28
28
  SERVER_STATUS_ROUTE = '/maint/state'
29
- SERVER_CLENUP_ROUTE = '/maint/cleanup'
29
+ SERVER_CLEANUP_ROUTE = '/maint/cleanup'
30
30
  SERVER_ROTATE_LOGS_ROUTE = '/maint/rotate-logs'
31
31
  SERVER_UPDATE_CLIENT_DATA_ROUTE = '/maint/update-client-data'
32
32
  SERVER_LOG_LEVEL_ROUTE = '/maint/set-log-level'
@@ -72,6 +72,14 @@ module Xolo
72
72
  puts msg unless quiet?
73
73
  end
74
74
 
75
+ # TODO: Use this everywhere in admin
76
+ ##########################
77
+ def show_debug(msg)
78
+ return unless debug?
79
+
80
+ puts "DEBUG: #{msg}"
81
+ end
82
+
75
83
  # Search for a title in Xolo
76
84
  # Looks for the search string (case insensitive) in these attributes:
77
85
  # - title
@@ -234,19 +242,30 @@ module Xolo
234
242
 
235
243
  new_title = Xolo::Admin::Title.new opts_to_process
236
244
 
245
+ # TMP TESTING
246
+ # puts 'DEBUG: opts_to_process:'
247
+ # opts_to_process.to_h.each do |k, v|
248
+ # puts " #{k}: #{v}"
249
+ # end
250
+ # return
251
+
237
252
  response_data = new_title.add(server_cnx)
238
253
 
239
254
  if debug?
240
255
  puts "DEBUG: response_data: #{response_data}"
241
256
  puts
242
257
  end
243
-
244
- display_progress response_data[:progress_stream_url_path]
258
+ if response_data[:progress_stream_url_path]
259
+ display_progress response_data[:progress_stream_url_path]
260
+ else
261
+ puts '# No progress stream URL returned'
262
+ return
263
+ end
245
264
 
246
265
  # Upload the ssvc icon, if any?
247
266
  upload_ssvc_icon new_title
248
267
 
249
- speak "Title '#{cli_cmd.title}' has been added to Xolo.\nAdd at least one version to enable piloting and deployment"
268
+ speak "Title '#{cli_cmd.title}' has been added to Xolo.\nEnsure there's at least one version to enable piloting and deployment"
250
269
  rescue StandardError => e
251
270
  handle_processing_error e
252
271
  end
@@ -307,9 +326,26 @@ module Xolo
307
326
  def upload_ssvc_icon(title)
308
327
  return unless title.self_service_icon.is_a? Pathname
309
328
 
329
+ speak 'Pausing to let jamf server catch up before uploading self-service icon...'
330
+ sleep 30
310
331
  speak "Uploading self-service icon #{title.self_service_icon.basename}, #{title.self_service_icon.pix_humanize_size} to Xolo."
311
332
 
312
- title.upload_self_service_icon(upload_cnx)
333
+ wait_for_title_timer = 0
334
+ begin
335
+ title.upload_self_service_icon(upload_cnx)
336
+ rescue Xolo::ConnectionError => e
337
+ speak "Error uploading self-service icon: #{e.message}, trying again in 5 secs"
338
+ # If we get a 404, it might be because the title isn't fully created yet.
339
+ # Wait a bit and try again, up to a certain number of seconds.
340
+ if wait_for_title_timer < 90
341
+ sleep 5
342
+ wait_for_title_timer += 5
343
+ retry
344
+ else
345
+ msg = "Failed to upload icon for Title '#{title}' after retrying for #{wait_for_title_timer} seconds."
346
+ raise Xolo::NoSuchItemError, msg
347
+ end # if
348
+ end # begin
313
349
 
314
350
  speak 'Self-service icon uploaded. Will be added to Self Service policies as needed'
315
351
  rescue StandardError => e
@@ -478,6 +514,7 @@ module Xolo
478
514
  opts_to_process.title = cli_cmd.title
479
515
  opts_to_process.version = cli_cmd.version
480
516
 
517
+ title_obj = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
481
518
  new_vers = Xolo::Admin::Version.new opts_to_process
482
519
  response_data = new_vers.add(server_cnx)
483
520
 
@@ -489,7 +526,8 @@ module Xolo
489
526
  display_progress response_data[:progress_stream_url_path]
490
527
 
491
528
  # Upload the pkg, if any?
492
- upload_pkg(new_vers)
529
+ upload_pkg(new_vers) unless title_obj.autopkg_enabled?
530
+
493
531
  speak 'It can take up to 15 minutes for the version to be available via Jamf and Self Service.'
494
532
  rescue StandardError => e
495
533
  handle_processing_error e
@@ -510,6 +548,8 @@ module Xolo
510
548
  def upload_pkg(version)
511
549
  return unless version.pkg_to_upload.is_a? Pathname
512
550
 
551
+ # TODO: deal with autopkg pkgs, should we be able to do this?
552
+
513
553
  speak "Uploading #{version.pkg_to_upload.basename}, #{version.pkg_to_upload.pix_humanize_size} to Xolo"
514
554
  # start the upload in a thread
515
555
  upload_thr = Thread.new { version.upload_pkg(upload_cnx) }
@@ -941,7 +981,7 @@ module Xolo
941
981
  def server_cleanup
942
982
  return unless confirmed? 'Run the Xolo Server cleanup process'
943
983
 
944
- result = server_cnx.post(SERVER_CLENUP_ROUTE).body
984
+ result = server_cnx.post(SERVER_CLEANUP_ROUTE).body
945
985
  puts result[:result]
946
986
  rescue StandardError => e
947
987
  handle_processing_error e
@@ -1036,6 +1076,35 @@ module Xolo
1036
1076
  list_in_cols header, jamf_computer_group_names.sort_by(&:downcase)
1037
1077
  end
1038
1078
 
1079
+ # List Available titles for subscription
1080
+ #
1081
+ # @return [void]
1082
+ #############################
1083
+ def list_available_titles
1084
+ data = jamf_available_titles.sort_by { |t| t[:app_name].downcase }
1085
+ data.map! do |t|
1086
+ {
1087
+ display_name: t[:app_name],
1088
+ publisher: t[:publisher],
1089
+ patch_source: t[:source_name],
1090
+ title_id: t[:name_id],
1091
+ current_version: t[:current_version],
1092
+ last_modified: t[:last_modified]
1093
+ }
1094
+ end
1095
+
1096
+ if json?
1097
+ puts JSON.pretty_generate(data)
1098
+ return
1099
+ end
1100
+
1101
+ title = 'Titles Available for Subscription'
1102
+ header = %w[DisplayName Publisher PatchSource TitleID]
1103
+ lines = data.map { |t| [t[:display_name], t[:publisher], t[:patch_source], t[:title_id]] }
1104
+
1105
+ show_text generate_report(lines, header_row: header, title: title)
1106
+ end
1107
+
1039
1108
  # List all the SSVC categories in jamf pro
1040
1109
  #
1041
1110
  # @return [void]
@@ -1079,7 +1148,7 @@ module Xolo
1079
1148
  end
1080
1149
 
1081
1150
  # run the cleanup
1082
- # get the /test route to do whatever testing it does
1151
+ # get the /test route to do whatever testing it does.
1083
1152
  # during testing - this will return all kinds of things.
1084
1153
  #
1085
1154
  # @return [void]
@@ -1104,11 +1173,7 @@ module Xolo
1104
1173
  display_progress resp[:progress_stream_url_path]
1105
1174
  end
1106
1175
 
1107
- # test uploads
1108
- # 1.2 gb
1109
- large_file = '/dist/caspershare/Packages-DEACTIVATED/SecUpd2020-006HighSierra.pkg'
1110
-
1111
- pkg_to_upload = Pathname.new large_file
1176
+ pkg_to_upload = Pathname.new '/path/to/some/file.pkg'
1112
1177
  puts "Uploading Test File #{pkg_to_upload.size} bytes... "
1113
1178
  upload_test_file(pkg_to_upload)
1114
1179
 
@@ -85,7 +85,7 @@ module Xolo
85
85
  ###################
86
86
  def progress_history
87
87
  progress_history_file.pix_touch
88
- history = YAML.load progress_history_file.read
88
+ history = YAML.safe_load progress_history_file.read, permitted_classes: [Symbol, Time]
89
89
  history ||= {}
90
90
 
91
91
  now = Time.now
@@ -43,10 +43,17 @@ module Xolo
43
43
  #############################
44
44
 
45
45
  # @return [Hash{Symbol: Hash}] The ATTRIBUTES that are available as CLI & walkthru options
46
+ ##############################
46
47
  def self.cli_opts
47
48
  @cli_opts ||= ATTRIBUTES.select { |_k, v| v[:cli] }
48
49
  end
49
50
 
51
+ # @return [Hash{Symbol: Hash}] The ATTRIBUTES that are available as opts for the subscribe command
52
+ ################################
53
+ # def self.subscribe_cli_opts
54
+ # @subscribe_cli_opts ||= ATTRIBUTES.select { |_k, v| v[:cli_subscribe] }
55
+ # end
56
+
50
57
  # @return [Array<String>] The currently known titles names on the server
51
58
  #############################
52
59
  def self.all_titles(cnx)
@@ -77,8 +84,11 @@ module Xolo
77
84
  ####################
78
85
  def self.fetch(title, cnx)
79
86
  resp = cnx.get "#{SERVER_ROUTE}/#{title}"
80
-
81
- new resp.body
87
+ title_obj = new resp.body
88
+ title_obj.cnx = cnx
89
+ title_obj
90
+ rescue Faraday::ResourceNotFound
91
+ raise Xolo::NoSuchItemError, "No such title '#{title}'"
82
92
  end
83
93
 
84
94
  # Delete a title from the server
@@ -113,6 +123,8 @@ module Xolo
113
123
  # Attributes
114
124
  ######################
115
125
  ######################
126
+ # the server connection used to fetch this version
127
+ attr_accessor :cnx
116
128
 
117
129
  # Constructor
118
130
  ######################
@@ -137,7 +149,7 @@ module Xolo
137
149
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
138
150
  # @return [Hash] the response body from the server
139
151
  ####################
140
- def add(cnx)
152
+ def add(cnx = self.cnx)
141
153
  resp = cnx.post SERVER_ROUTE, to_h
142
154
  resp.body
143
155
  end
@@ -146,7 +158,7 @@ module Xolo
146
158
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
147
159
  # @return [Hash] the response body from the server
148
160
  ####################
149
- def update(cnx)
161
+ def update(cnx = self.cnx)
150
162
  resp = cnx.put "#{SERVER_ROUTE}/#{title}", to_h
151
163
  resp.body
152
164
  end
@@ -156,7 +168,7 @@ module Xolo
156
168
  # @param version [String] the version to release
157
169
  # @return [Hash] the response body from the server
158
170
  ####################
159
- def release(cnx, version:)
171
+ def release(cnx = self.cnx, version:)
160
172
  resp = cnx.patch "#{SERVER_ROUTE}/#{title}/release/#{version}", {}
161
173
  resp.body
162
174
  end
@@ -166,7 +178,7 @@ module Xolo
166
178
  # @param versions [Boolean] if true, repair all versions of this title
167
179
  # @return [Hash] the response body from the server
168
180
  ####################
169
- def repair(cnx, versions: false)
181
+ def repair(cnx = self.cnx, versions: false)
170
182
  resp = cnx.post "#{SERVER_ROUTE}/#{title}/repair", { repair_versions: versions }
171
183
  resp.body
172
184
  end
@@ -175,7 +187,7 @@ module Xolo
175
187
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
176
188
  # @return [Hash] the response data
177
189
  ####################
178
- def delete(cnx)
190
+ def delete(cnx = self.cnx)
179
191
  self.class.delete title, cnx
180
192
  end
181
193
 
@@ -184,7 +196,7 @@ module Xolo
184
196
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
185
197
  # @return [Hash] the response data
186
198
  ####################
187
- def freeze(targets, users = false, cnx)
199
+ def freeze(targets, users = false, cnx = self.cnx)
188
200
  data = { targets: targets, users: users }
189
201
  resp = cnx.put "#{SERVER_ROUTE}/#{title}/freeze", data
190
202
  resp.body
@@ -195,7 +207,7 @@ module Xolo
195
207
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
196
208
  # @return [Hash] the response data
197
209
  ####################
198
- def thaw(targets, users = false, cnx)
210
+ def thaw(targets, users = false, cnx = self.cnx)
199
211
  data = { targets: targets, users: users }
200
212
  resp = cnx.put "#{SERVER_ROUTE}/#{title}/thaw", data
201
213
  resp.body
@@ -205,7 +217,7 @@ module Xolo
205
217
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
206
218
  # @return [Hash{String => String}] computer name => user name
207
219
  ####################
208
- def frozen(cnx)
220
+ def frozen(cnx = self.cnx)
209
221
  resp = cnx.get "#{SERVER_ROUTE}/#{title}/frozen"
210
222
  resp.body
211
223
  end
@@ -214,7 +226,7 @@ module Xolo
214
226
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
215
227
  # @return [Hash{String => String}] page_name => url
216
228
  ####################
217
- def gui_urls(cnx)
229
+ def gui_urls(cnx = self.cnx)
218
230
  resp = cnx.get "#{SERVER_ROUTE}/#{title}/urls"
219
231
  resp.body
220
232
  end
@@ -225,7 +237,7 @@ module Xolo
225
237
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
226
238
  # @return [Array<Hash>] The change log for this title
227
239
  ####################
228
- def changelog(cnx)
240
+ def changelog(cnx = self.cnx)
229
241
  resp = cnx.get "#{SERVER_ROUTE}/#{title}/changelog"
230
242
  resp.body
231
243
  end
@@ -246,19 +258,16 @@ module Xolo
246
258
  "Can't upload self service icon '#{self_service_icon}': file doesn't exist or is not readable"
247
259
  end
248
260
 
249
- # upfile = Faraday::UploadIO.new(
250
- # self_service_icon.to_s,
251
- # 'application/octet-stream',
252
- # self_service_icon.basename.to_s
253
- # )
254
-
255
261
  mimetype = `/usr/bin/file --brief --mime-type #{Shellwords.escape self_service_icon.expand_path.to_s}`.chomp
256
262
  upfile = Faraday::Multipart::FilePart.new(self_service_icon.expand_path.to_s, mimetype)
257
263
  content = { file: upfile }
258
- # route = "#{UPLOAD_ICON_ROUTE}/#{title}"
259
- route = "#{SERVER_ROUTE}/#{title}/#{UPLOAD_ICON_ROUTE}"
260
264
 
265
+ route = "#{SERVER_ROUTE}/#{title}/#{UPLOAD_ICON_ROUTE}"
261
266
  upload_cnx.post(route) { |req| req.body = content }
267
+ rescue Xolo::NoSuchItemError
268
+ raise
269
+ rescue StandardError => e
270
+ raise Xolo::ConnectionError, "#{e.class}: #{e.message}"
262
271
  end
263
272
 
264
273
  # Get the patch report data for this title
@@ -267,16 +276,16 @@ module Xolo
267
276
  # @param cnx [Faraday::Connection] The connection to use, must be logged in already
268
277
  # @return [Array<Hash>] Data for each computer with any version of this title installed
269
278
  ##################################
270
- def patch_report_data(cnx)
279
+ def patch_report_data(cnx = self.cnx)
271
280
  resp = cnx.get "#{SERVER_ROUTE}/#{title}/patch_report"
272
281
  resp.body
273
282
  end
274
283
 
275
284
  # Add more data to our hash
276
285
  ###########################
277
- def to_h
278
- super
279
- end
286
+ # def to_h
287
+ # super
288
+ # end
280
289
 
281
290
  end # class Title
282
291