xolo-admin 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +5 -0
- data/bin/xadm +114 -0
- data/lib/xolo/admin/command_line.rb +432 -0
- data/lib/xolo/admin/configuration.rb +196 -0
- data/lib/xolo/admin/connection.rb +191 -0
- data/lib/xolo/admin/cookie_jar.rb +81 -0
- data/lib/xolo/admin/credentials.rb +212 -0
- data/lib/xolo/admin/highline_terminal.rb +81 -0
- data/lib/xolo/admin/interactive.rb +762 -0
- data/lib/xolo/admin/jamf_pro.rb +75 -0
- data/lib/xolo/admin/options.rb +1139 -0
- data/lib/xolo/admin/processing.rb +1329 -0
- data/lib/xolo/admin/progress_history.rb +117 -0
- data/lib/xolo/admin/title.rb +285 -0
- data/lib/xolo/admin/title_editor.rb +57 -0
- data/lib/xolo/admin/validate.rb +1032 -0
- data/lib/xolo/admin/version.rb +221 -0
- data/lib/xolo/admin.rb +139 -0
- data/lib/xolo-admin.rb +8 -0
- metadata +139 -0
|
@@ -0,0 +1,1329 @@
|
|
|
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 Admin
|
|
14
|
+
|
|
15
|
+
# Methods that execute the xadm commands and their options
|
|
16
|
+
#
|
|
17
|
+
module Processing
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
##########################
|
|
21
|
+
##########################
|
|
22
|
+
|
|
23
|
+
# Title attributes that are used for 'xadm search'
|
|
24
|
+
SEARCH_ATTRIBUTES = %i[title display_name description publisher app_name app_bundle_id].freeze
|
|
25
|
+
|
|
26
|
+
# Routes for server admins
|
|
27
|
+
|
|
28
|
+
SERVER_STATUS_ROUTE = '/maint/state'
|
|
29
|
+
SERVER_CLENUP_ROUTE = '/maint/cleanup'
|
|
30
|
+
SERVER_ROTATE_LOGS_ROUTE = '/maint/rotate-logs'
|
|
31
|
+
SERVER_UPDATE_CLIENT_DATA_ROUTE = '/maint/update-client-data'
|
|
32
|
+
SERVER_LOG_LEVEL_ROUTE = '/maint/set-log-level'
|
|
33
|
+
SERVER_SHUTDOWN_ROUTE = '/maint/shutdown-server'
|
|
34
|
+
|
|
35
|
+
# We can't pull this from Xolo::Server::Log::LEVELS, because
|
|
36
|
+
# we don't want to require that file here, it'll complain about
|
|
37
|
+
# the lack of sinatra and such.
|
|
38
|
+
LOG_LEVELS = %w[debug info warn error fatal].freeze
|
|
39
|
+
|
|
40
|
+
# Module Methods
|
|
41
|
+
##########################
|
|
42
|
+
##########################
|
|
43
|
+
|
|
44
|
+
# when this module is included
|
|
45
|
+
def self.included(includer)
|
|
46
|
+
Xolo.verbose_include includer, self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# when this module is extended
|
|
50
|
+
def self.extended(extender)
|
|
51
|
+
Xolo.verbose_extend extender, self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Instance Methods
|
|
55
|
+
##########################
|
|
56
|
+
##########################
|
|
57
|
+
|
|
58
|
+
# Which opts to process, those from walkthru, or from the CLI?
|
|
59
|
+
#
|
|
60
|
+
# @return [OpenStruct] the opts to process
|
|
61
|
+
#######################
|
|
62
|
+
def opts_to_process
|
|
63
|
+
@opts_to_process ||= walkthru? ? walkthru_cmd_opts : cli_cmd_opts
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# puts a string to stdout, unless quiet? is true
|
|
67
|
+
#
|
|
68
|
+
# @param msg [String] the string to puts
|
|
69
|
+
# @return [void]
|
|
70
|
+
#########################
|
|
71
|
+
def speak(msg)
|
|
72
|
+
puts msg unless quiet?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Search for a title in Xolo
|
|
76
|
+
# Looks for the search string (case insensitive) in these attributes:
|
|
77
|
+
# - title
|
|
78
|
+
# - display_name
|
|
79
|
+
# - description
|
|
80
|
+
# - publisher
|
|
81
|
+
# - app_name
|
|
82
|
+
# - app_bundle_id
|
|
83
|
+
#
|
|
84
|
+
# will output the results showing those attributes
|
|
85
|
+
#
|
|
86
|
+
# If json? is true, will output the results as a JSON array of hashes
|
|
87
|
+
# containing the full title object.
|
|
88
|
+
#
|
|
89
|
+
# @param search_str [String] the string to search for
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
92
|
+
###############################
|
|
93
|
+
def search_titles
|
|
94
|
+
search_str = cli_cmd.title
|
|
95
|
+
titles = Xolo::Admin::Title.all_title_objects(server_cnx)
|
|
96
|
+
|
|
97
|
+
results = []
|
|
98
|
+
titles.each do |t|
|
|
99
|
+
next unless SEARCH_ATTRIBUTES.any? { |attr| t.send(attr).to_s =~ /#{search_str}/i }
|
|
100
|
+
|
|
101
|
+
if json?
|
|
102
|
+
results << t.to_h
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results << title_search_result_str(t, one_line: cli_cmd_opts.summary)
|
|
107
|
+
end # titles.each
|
|
108
|
+
|
|
109
|
+
# if json?, output it and we're done
|
|
110
|
+
if json?
|
|
111
|
+
puts JSON.pretty_generate(results)
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# no results?
|
|
116
|
+
if results.empty?
|
|
117
|
+
puts "# No titles matching '#{search_str}'"
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# results found
|
|
122
|
+
puts "# All titles matching '#{search_str}'"
|
|
123
|
+
if cli_cmd_opts.summary
|
|
124
|
+
header = %w[Title Display Publisher Contact Versions]
|
|
125
|
+
show_text generate_report(results, header_row: header, title: nil)
|
|
126
|
+
else
|
|
127
|
+
puts results.join("\n\n")
|
|
128
|
+
end
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
handle_processing_error e
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# From a title, get a String for use in a search report, either
|
|
134
|
+
# multiline or single line
|
|
135
|
+
# @param title [Xolo::Admin::Title] the title
|
|
136
|
+
# @param one_line [Boolean] whether to use single line format
|
|
137
|
+
# @return [String] the string to display for the search result
|
|
138
|
+
###################################
|
|
139
|
+
def title_search_result_str(title, one_line: false)
|
|
140
|
+
versions = versions_str(title)
|
|
141
|
+
if one_line
|
|
142
|
+
[title.title, title.display_name, title.publisher, title.contact_email, versions]
|
|
143
|
+
else
|
|
144
|
+
titleout = +'#---------------------------------------'
|
|
145
|
+
titleout << "\nTitle: #{title.title}"
|
|
146
|
+
titleout << "\nDisplay Name: #{title.display_name}"
|
|
147
|
+
titleout << "\nPublisher: #{title.publisher}"
|
|
148
|
+
titleout << "\nApp: #{title.app_name}\nBundleID: #{title.app_bundle_id}" if title.app_name
|
|
149
|
+
titleout << "\nVersions: #{versions}"
|
|
150
|
+
titleout << "\nDescription:"
|
|
151
|
+
titleout << "\n#{title.description}"
|
|
152
|
+
titleout
|
|
153
|
+
end # json?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# From a title, get a String with current versions of a title, comma separated
|
|
157
|
+
# with the current released version marked
|
|
158
|
+
# @param title [Xolo::Admin::Title] the title
|
|
159
|
+
# @return [String] the versions string
|
|
160
|
+
##################################
|
|
161
|
+
def versions_str(title)
|
|
162
|
+
versions = []
|
|
163
|
+
title.version_order.each do |v|
|
|
164
|
+
versions <<
|
|
165
|
+
if v.to_s == title.released_version.to_s
|
|
166
|
+
"#{v} (released)"
|
|
167
|
+
else
|
|
168
|
+
v
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
versions.join(', ')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# update the adm config file using the values from 'xadm config'
|
|
175
|
+
# which is a walkthru
|
|
176
|
+
#
|
|
177
|
+
# @return [void]
|
|
178
|
+
###############################
|
|
179
|
+
def update_config
|
|
180
|
+
debug? && puts("DEBUG: opts_to_process: #{opts_to_process}")
|
|
181
|
+
|
|
182
|
+
# Xolo::Admin::Configuration::KEYS.each_key do |key|
|
|
183
|
+
# config.send "#{key}=", opts_to_process[key]
|
|
184
|
+
# end
|
|
185
|
+
|
|
186
|
+
config.save_to_file data: opts_to_process.to_h
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# List all titles in Xolo
|
|
190
|
+
#
|
|
191
|
+
# @return [void]
|
|
192
|
+
###############################
|
|
193
|
+
def list_titles
|
|
194
|
+
titles = Xolo::Admin::Title.all_title_objects(server_cnx)
|
|
195
|
+
|
|
196
|
+
if json?
|
|
197
|
+
puts JSON.pretty_generate(titles.map(&:to_h))
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if titles.empty?
|
|
202
|
+
puts "# No Titles in Xolo! Add one with 'xadm add-title <title>'"
|
|
203
|
+
return
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
report_title = 'All titles in Xolo'
|
|
207
|
+
header = %w[Title Created By SSvc? Released Latest]
|
|
208
|
+
data = titles.map do |t|
|
|
209
|
+
[
|
|
210
|
+
t.title,
|
|
211
|
+
t.creation_date.to_date,
|
|
212
|
+
t.created_by,
|
|
213
|
+
t.self_service || false,
|
|
214
|
+
t.released_version,
|
|
215
|
+
t.latest_version
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
data.sort_by! { |d| d[0].downcase } # sort by title
|
|
219
|
+
|
|
220
|
+
show_text generate_report(data, header_row: header, title: report_title)
|
|
221
|
+
rescue StandardError => e
|
|
222
|
+
handle_processing_error e
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Add a title to Xolo
|
|
226
|
+
#
|
|
227
|
+
# @return [void]
|
|
228
|
+
###############################
|
|
229
|
+
def add_title
|
|
230
|
+
return unless confirmed? "Add title '#{cli_cmd.title}'"
|
|
231
|
+
|
|
232
|
+
opts_to_process.title = cli_cmd.title
|
|
233
|
+
read_uninstall_script
|
|
234
|
+
|
|
235
|
+
new_title = Xolo::Admin::Title.new opts_to_process
|
|
236
|
+
|
|
237
|
+
response_data = new_title.add(server_cnx)
|
|
238
|
+
|
|
239
|
+
if debug?
|
|
240
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
241
|
+
puts
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
display_progress response_data[:progress_stream_url_path]
|
|
245
|
+
|
|
246
|
+
# Upload the ssvc icon, if any?
|
|
247
|
+
upload_ssvc_icon new_title
|
|
248
|
+
|
|
249
|
+
speak "Title '#{cli_cmd.title}' has been added to Xolo.\nAdd at least one version to enable piloting and deployment"
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
handle_processing_error e
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Edit/Update a title in Xolo
|
|
255
|
+
#
|
|
256
|
+
# @return [void]
|
|
257
|
+
###############################
|
|
258
|
+
def edit_title
|
|
259
|
+
return unless confirmed? "Edit title '#{cli_cmd.title}'"
|
|
260
|
+
|
|
261
|
+
opts_to_process.title = cli_cmd.title
|
|
262
|
+
read_uninstall_script
|
|
263
|
+
|
|
264
|
+
# if we were passed a new uninstall script, remove the uninstall ids
|
|
265
|
+
if opts_to_process.uninstall_script && opts_to_process.uninstall_script != Xolo::ITEM_UPLOADED
|
|
266
|
+
opts_to_process.uninstall_ids = []
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# if we were passed uninstall ids, remove the uninstall script
|
|
270
|
+
opts_to_process.uninstall_script = nil unless opts_to_process.uninstall_ids.pix_empty?
|
|
271
|
+
|
|
272
|
+
title = Xolo::Admin::Title.new opts_to_process
|
|
273
|
+
response_data = title.update server_cnx
|
|
274
|
+
|
|
275
|
+
if debug?
|
|
276
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
277
|
+
puts
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
display_progress response_data[:progress_stream_url_path]
|
|
281
|
+
|
|
282
|
+
# Upload the ssvc icon, if any?
|
|
283
|
+
upload_ssvc_icon title
|
|
284
|
+
|
|
285
|
+
speak "Title '#{cli_cmd.title}' has been updated in Xolo."
|
|
286
|
+
rescue StandardError => e
|
|
287
|
+
handle_processing_error e
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# if we have an uninstall script, read it in
|
|
291
|
+
#
|
|
292
|
+
# @return [void]
|
|
293
|
+
###############################
|
|
294
|
+
def read_uninstall_script
|
|
295
|
+
return unless opts_to_process.uninstall_script.to_s.start_with? '/'
|
|
296
|
+
|
|
297
|
+
opts_to_process.uninstall_script = Pathname.new(opts_to_process.uninstall_script).read
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Upload the ssvc icon, if any?
|
|
301
|
+
# TODO: progress feedback? Icons should never be very large, so
|
|
302
|
+
# prob. not, to start with
|
|
303
|
+
#
|
|
304
|
+
# @param title [Xolo::Admin::Title] the title for which we are uploading an icon
|
|
305
|
+
# @return [void]
|
|
306
|
+
#######################
|
|
307
|
+
def upload_ssvc_icon(title)
|
|
308
|
+
return unless title.self_service_icon.is_a? Pathname
|
|
309
|
+
|
|
310
|
+
speak "Uploading self-service icon #{title.self_service_icon.basename}, #{title.self_service_icon.pix_humanize_size} to Xolo."
|
|
311
|
+
|
|
312
|
+
title.upload_self_service_icon(upload_cnx)
|
|
313
|
+
|
|
314
|
+
speak 'Self-service icon uploaded. Will be added to Self Service policies as needed'
|
|
315
|
+
rescue StandardError => e
|
|
316
|
+
handle_processing_error e
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Delete a title in Xolo
|
|
320
|
+
#
|
|
321
|
+
# @return [void]
|
|
322
|
+
###############################
|
|
323
|
+
def delete_title
|
|
324
|
+
return unless confirmed? "Delete title '#{cli_cmd.title}' and all its versions"
|
|
325
|
+
|
|
326
|
+
response_data = Xolo::Admin::Title.delete cli_cmd.title, server_cnx
|
|
327
|
+
|
|
328
|
+
if debug?
|
|
329
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
330
|
+
puts
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
display_progress response_data[:progress_stream_url_path]
|
|
334
|
+
|
|
335
|
+
speak "Title '#{cli_cmd.title}' has been deleted from Xolo."
|
|
336
|
+
rescue StandardError => e
|
|
337
|
+
handle_processing_error e
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Freeze one or more computers for a title in Xolo
|
|
341
|
+
#
|
|
342
|
+
# @return [void]
|
|
343
|
+
###############################
|
|
344
|
+
def freeze
|
|
345
|
+
conf_msg = <<~ENDMSG
|
|
346
|
+
Freeze computers for title '#{cli_cmd.title}'.
|
|
347
|
+
They will not update beyond their installed version,
|
|
348
|
+
even it the title is not installed at all.
|
|
349
|
+
ENDMSG
|
|
350
|
+
|
|
351
|
+
return unless confirmed? conf_msg
|
|
352
|
+
|
|
353
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
354
|
+
response_data = title.freeze ARGV, cli_cmd_opts.users_given, server_cnx
|
|
355
|
+
|
|
356
|
+
if debug?
|
|
357
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
358
|
+
puts
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if json?
|
|
362
|
+
puts JSON.pretty_generate(response_data)
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
rpt_title = "Results for freezing Title '#{cli_cmd.title}'"
|
|
367
|
+
header = %w[Computer Result]
|
|
368
|
+
report = generate_report(response_data.to_a, header_row: header, title: rpt_title)
|
|
369
|
+
show_text report
|
|
370
|
+
rescue StandardError => e
|
|
371
|
+
handle_processing_error e
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# list the computers that are frozen for a title in Xolo
|
|
375
|
+
#
|
|
376
|
+
# @return [void]
|
|
377
|
+
###############################
|
|
378
|
+
def list_frozen
|
|
379
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
380
|
+
frozen_computers = title.frozen server_cnx
|
|
381
|
+
if debug?
|
|
382
|
+
puts "DEBUG: response_data: #{frozen_computers}"
|
|
383
|
+
puts
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
if json?
|
|
387
|
+
puts JSON.pretty_generate(frozen_computers)
|
|
388
|
+
return
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if frozen_computers.empty?
|
|
392
|
+
puts "# No computers are frozen for Title '#{cli_cmd.title}'"
|
|
393
|
+
return
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
rpt_title = "Frozen Computers for Title '#{cli_cmd.title}'"
|
|
397
|
+
header = %w[Computer User]
|
|
398
|
+
report = generate_report(frozen_computers.to_a, header_row: header, title: rpt_title)
|
|
399
|
+
show_text report
|
|
400
|
+
rescue StandardError => e
|
|
401
|
+
handle_processing_error e
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Thaw one or more computers for a title in Xolo
|
|
405
|
+
#
|
|
406
|
+
# @return [void]
|
|
407
|
+
###############################
|
|
408
|
+
def thaw
|
|
409
|
+
conf_msg = <<~ENDMSG
|
|
410
|
+
Thaw computers for title '#{cli_cmd.title}'.
|
|
411
|
+
They will again be able to update beyond their installed version.
|
|
412
|
+
ENDMSG
|
|
413
|
+
|
|
414
|
+
return unless confirmed? conf_msg
|
|
415
|
+
|
|
416
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
417
|
+
response_data = title.thaw ARGV, cli_cmd_opts.users_given, server_cnx
|
|
418
|
+
|
|
419
|
+
if debug?
|
|
420
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
421
|
+
puts
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
if json?
|
|
425
|
+
puts JSON.pretty_generate(response_data)
|
|
426
|
+
return
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
rpt_title = "Results for thawing Title '#{cli_cmd.title}'"
|
|
430
|
+
header = %w[Computer Result]
|
|
431
|
+
report = generate_report(response_data.to_a, header_row: header, title: rpt_title)
|
|
432
|
+
show_text report
|
|
433
|
+
rescue StandardError => e
|
|
434
|
+
handle_processing_error e
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# List all versions of a title in Xolo
|
|
438
|
+
#
|
|
439
|
+
# @return [void]
|
|
440
|
+
###############################
|
|
441
|
+
def list_versions
|
|
442
|
+
versions = Xolo::Admin::Version.all_version_objects(cli_cmd.title, server_cnx)
|
|
443
|
+
|
|
444
|
+
if json?
|
|
445
|
+
puts JSON.pretty_generate(versions.map(&:to_h))
|
|
446
|
+
return
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if versions.empty?
|
|
450
|
+
puts "# No versions for Title '#{cli_cmd.title}'"
|
|
451
|
+
return
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
report_title = "All versions of '#{cli_cmd.title}' in Xolo"
|
|
455
|
+
header = %w[Vers Created By Released By Status]
|
|
456
|
+
data = versions.sort_by(&:creation_date).map do |v|
|
|
457
|
+
[
|
|
458
|
+
v.version,
|
|
459
|
+
v.creation_date.to_date,
|
|
460
|
+
v.created_by,
|
|
461
|
+
v.release_date&.to_date,
|
|
462
|
+
v.released_by,
|
|
463
|
+
v.status
|
|
464
|
+
]
|
|
465
|
+
end
|
|
466
|
+
show_text generate_report(data, header_row: header, title: report_title)
|
|
467
|
+
rescue StandardError => e
|
|
468
|
+
handle_processing_error e
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Add a version to a title to Xolo
|
|
472
|
+
#
|
|
473
|
+
# @return [void]
|
|
474
|
+
###############################
|
|
475
|
+
def add_version
|
|
476
|
+
return unless confirmed? "Add version '#{cli_cmd.version}' to title '#{cli_cmd.title}'"
|
|
477
|
+
|
|
478
|
+
opts_to_process.title = cli_cmd.title
|
|
479
|
+
opts_to_process.version = cli_cmd.version
|
|
480
|
+
|
|
481
|
+
new_vers = Xolo::Admin::Version.new opts_to_process
|
|
482
|
+
response_data = new_vers.add(server_cnx)
|
|
483
|
+
|
|
484
|
+
if debug?
|
|
485
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
486
|
+
puts
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
display_progress response_data[:progress_stream_url_path]
|
|
490
|
+
|
|
491
|
+
# Upload the pkg, if any?
|
|
492
|
+
upload_pkg(new_vers)
|
|
493
|
+
speak 'It can take up to 15 minutes for the version to be available via Jamf and Self Service.'
|
|
494
|
+
rescue StandardError => e
|
|
495
|
+
handle_processing_error e
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Upload a pkg in a thread with indeterminate progress feedback
|
|
499
|
+
# (i.e. '...Upload in progress' )
|
|
500
|
+
# Determining actual progress numbers
|
|
501
|
+
# would require either a locol tool to meter the IO or a server
|
|
502
|
+
# capable of sending it as a progress stream, neither of which
|
|
503
|
+
# is straightforward.
|
|
504
|
+
#
|
|
505
|
+
# @param version [Xolo::Admin::Version] the version for which we are uploading
|
|
506
|
+
# the pkg. It must have a 'pkg_to_upload' that is a pathname to an existing
|
|
507
|
+
# file
|
|
508
|
+
# @return [void]
|
|
509
|
+
################################
|
|
510
|
+
def upload_pkg(version)
|
|
511
|
+
return unless version.pkg_to_upload.is_a? Pathname
|
|
512
|
+
|
|
513
|
+
speak "Uploading #{version.pkg_to_upload.basename}, #{version.pkg_to_upload.pix_humanize_size} to Xolo"
|
|
514
|
+
# start the upload in a thread
|
|
515
|
+
upload_thr = Thread.new { version.upload_pkg(upload_cnx) }
|
|
516
|
+
|
|
517
|
+
# check the thread every second, but only update the terminal every 10 secs
|
|
518
|
+
count = 0
|
|
519
|
+
while upload_thr.alive?
|
|
520
|
+
|
|
521
|
+
speak "... #{Time.now.strftime '%F %T'} Upload in progress" if (count % 10).zero?
|
|
522
|
+
sleep 1
|
|
523
|
+
count += 1
|
|
524
|
+
end
|
|
525
|
+
speak 'Upload complete, Final upload to distribution points will happen soon.'
|
|
526
|
+
rescue StandardError => e
|
|
527
|
+
handle_processing_error e
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Edit/Update a version in Xolo
|
|
531
|
+
#
|
|
532
|
+
# @return [void]
|
|
533
|
+
###############################
|
|
534
|
+
def edit_version
|
|
535
|
+
return unless confirmed? "Edit Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
536
|
+
|
|
537
|
+
opts_to_process.title = cli_cmd.title
|
|
538
|
+
opts_to_process.version = cli_cmd.version
|
|
539
|
+
vers = Xolo::Admin::Version.new opts_to_process
|
|
540
|
+
|
|
541
|
+
response_data = vers.update server_cnx
|
|
542
|
+
|
|
543
|
+
if debug?
|
|
544
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
545
|
+
puts
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
display_progress response_data[:progress_stream_url_path]
|
|
549
|
+
|
|
550
|
+
# Upload the pkg, if any?
|
|
551
|
+
upload_pkg(vers)
|
|
552
|
+
|
|
553
|
+
speak "Version '#{cli_cmd.version}' of title '#{cli_cmd.title}' has been updated in Xolo."
|
|
554
|
+
rescue StandardError => e
|
|
555
|
+
handle_processing_error e
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Deploy a version of a title in Xolo to one or more computers or computer groups
|
|
559
|
+
#
|
|
560
|
+
# @return [void]
|
|
561
|
+
###############################
|
|
562
|
+
def deploy_version
|
|
563
|
+
return unless confirmed? "Deploy Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}' via MDM?"
|
|
564
|
+
|
|
565
|
+
unless json? || quiet?
|
|
566
|
+
puts "Deploying Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}' to computers: #{ARGV.join(', ')}"
|
|
567
|
+
puts "Groups: #{opts_to_process[:groups].join(', ')}" unless opts_to_process[:groups].pix_empty?
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
response = Xolo::Admin::Version.deploy(
|
|
571
|
+
cli_cmd.title,
|
|
572
|
+
cli_cmd.version,
|
|
573
|
+
server_cnx,
|
|
574
|
+
groups: opts_to_process[:groups],
|
|
575
|
+
computers: ARGV
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
return if quiet?
|
|
579
|
+
|
|
580
|
+
if json?
|
|
581
|
+
puts JSON.pretty_generate(response)
|
|
582
|
+
return
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
puts "Results: Deployment of Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
586
|
+
puts '---------------------------------------------------------------'
|
|
587
|
+
response[:removals].each do |removal|
|
|
588
|
+
type = removal[:device] ? 'Computer' : 'Group'
|
|
589
|
+
name = removal[:device] || removal[:group]
|
|
590
|
+
puts "Removed #{type} '#{name}' from targets: #{removal[:reason]}"
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
response[:queuedCommands].each do |cmd|
|
|
594
|
+
puts "Sent MDM deployment to #{cmd[:device]}, Command UUID: #{cmd[:commandUuid]}"
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
response[:errors].each do |err|
|
|
598
|
+
puts "Error deploying to #{err[:device]}: #{err[:reason]}"
|
|
599
|
+
end
|
|
600
|
+
rescue StandardError => e
|
|
601
|
+
handle_processing_error e
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Delete a title in Xolo
|
|
605
|
+
#
|
|
606
|
+
# @return [void]
|
|
607
|
+
###############################
|
|
608
|
+
def delete_version
|
|
609
|
+
return unless confirmed? "Delete version '#{cli_cmd.version}' from title '#{cli_cmd.title}'"
|
|
610
|
+
|
|
611
|
+
response_data = Xolo::Admin::Version.delete cli_cmd.title, cli_cmd.version, server_cnx
|
|
612
|
+
|
|
613
|
+
if debug?
|
|
614
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
615
|
+
puts
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
display_progress response_data[:progress_stream_url_path]
|
|
619
|
+
rescue StandardError => e
|
|
620
|
+
handle_processing_error e
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Release a version of a title in Xolo
|
|
624
|
+
#
|
|
625
|
+
# @return [void]
|
|
626
|
+
###############################
|
|
627
|
+
def release_version
|
|
628
|
+
return unless confirmed? "Release Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
629
|
+
|
|
630
|
+
opts_to_process.title = cli_cmd.title
|
|
631
|
+
|
|
632
|
+
title = Xolo::Admin::Title.new opts_to_process
|
|
633
|
+
response_data = title.release server_cnx, version: cli_cmd.version
|
|
634
|
+
|
|
635
|
+
if debug?
|
|
636
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
637
|
+
puts
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
display_progress response_data[:progress_stream_url_path]
|
|
641
|
+
speak "Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}' has been released."
|
|
642
|
+
rescue StandardError => e
|
|
643
|
+
handle_processing_error e
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# show the change log for a title
|
|
647
|
+
#
|
|
648
|
+
# @return [void]
|
|
649
|
+
###############################
|
|
650
|
+
def show_changelog
|
|
651
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
652
|
+
changelog = title.changelog(server_cnx)
|
|
653
|
+
if json?
|
|
654
|
+
puts JSON.pretty_generate(changelog)
|
|
655
|
+
return
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
output = ["# Changelog for Title '#{cli_cmd.title}'"]
|
|
659
|
+
output << '#' * (output.first.length + 5)
|
|
660
|
+
changelog.each do |change|
|
|
661
|
+
vers_or_title = change[:version] ? "version #{change[:version]}" : 'title'
|
|
662
|
+
output << "#{Time.parse(change[:time]).strftime('%F %T')} #{change[:admin]}@#{change[:host]} changed #{vers_or_title}"
|
|
663
|
+
|
|
664
|
+
if change[:msg]
|
|
665
|
+
val = format_multiline_indent(change[:msg], indent: 7)
|
|
666
|
+
output << " msg: #{val}"
|
|
667
|
+
|
|
668
|
+
else
|
|
669
|
+
output << " Attribute: #{change[:attrib]}"
|
|
670
|
+
from_val = format_multiline_indent(change[:old], indent: 8)
|
|
671
|
+
output << " From: #{from_val}"
|
|
672
|
+
to_val = format_multiline_indent(change[:new], indent: 6)
|
|
673
|
+
output << " To: #{to_val}"
|
|
674
|
+
end
|
|
675
|
+
output << nil
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
show_text output.join("\n")
|
|
679
|
+
rescue StandardError => e
|
|
680
|
+
handle_processing_error e
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Show details about a title or version in xolo
|
|
684
|
+
#
|
|
685
|
+
# @return [void]
|
|
686
|
+
###############################
|
|
687
|
+
def show_info
|
|
688
|
+
cli_cmd.version ? show_version_info : show_title_info
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Show details about a title in xolo
|
|
692
|
+
#
|
|
693
|
+
# @return [void]
|
|
694
|
+
###############################
|
|
695
|
+
def show_title_info
|
|
696
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
697
|
+
|
|
698
|
+
if json?
|
|
699
|
+
puts title.to_json
|
|
700
|
+
return
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
urls = title.gui_urls(server_cnx)
|
|
704
|
+
|
|
705
|
+
puts "# Info for Title '#{cli_cmd.title}'"
|
|
706
|
+
puts '###################################'
|
|
707
|
+
|
|
708
|
+
Xolo::Admin::Title::ATTRIBUTES.each do |attr, deets|
|
|
709
|
+
next if deets[:hide_from_info]
|
|
710
|
+
# description is handled specially below
|
|
711
|
+
next if attr == :description
|
|
712
|
+
|
|
713
|
+
value = title.send attr
|
|
714
|
+
value = value.join(Xolo::COMMA_JOIN) if value.is_a? Array
|
|
715
|
+
puts "- #{deets[:label]}: #{value}".pix_word_wrap
|
|
716
|
+
end
|
|
717
|
+
puts '- Description:'
|
|
718
|
+
title.description.lines.each { |line| puts " #{line.chomp}" } unless title.description.pix_empty?
|
|
719
|
+
|
|
720
|
+
puts '#'
|
|
721
|
+
puts '# Web App URLs'
|
|
722
|
+
puts '###################################'
|
|
723
|
+
urls.each { |pagename, url| puts "#{pagename}: #{url}" }
|
|
724
|
+
rescue StandardError => e
|
|
725
|
+
handle_processing_error e
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Show details about a title in xolo
|
|
729
|
+
#
|
|
730
|
+
# @return [void]
|
|
731
|
+
###############################
|
|
732
|
+
def show_version_info
|
|
733
|
+
vers = Xolo::Admin::Version.fetch cli_cmd.title, cli_cmd.version, server_cnx
|
|
734
|
+
|
|
735
|
+
if json?
|
|
736
|
+
puts vers.to_json
|
|
737
|
+
return
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
urls = vers.gui_urls(server_cnx)
|
|
741
|
+
|
|
742
|
+
puts "# Info for Version #{cli_cmd.version} of Title '#{cli_cmd.title}'"
|
|
743
|
+
puts '##################################################'
|
|
744
|
+
|
|
745
|
+
Xolo::Admin::Version::ATTRIBUTES.each do |attr, deets|
|
|
746
|
+
next if deets[:hide_from_info]
|
|
747
|
+
|
|
748
|
+
value = vers.send attr
|
|
749
|
+
value = value.join(Xolo::COMMA_JOIN) if value.is_a? Array
|
|
750
|
+
puts "- #{deets[:label]}: #{value}".pix_word_wrap
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
puts '#'
|
|
754
|
+
puts '# Web App URLs'
|
|
755
|
+
puts '###################################'
|
|
756
|
+
urls.each { |pagename, url| puts "#{pagename}: #{url}" }
|
|
757
|
+
# rescue Faraday::ResourceNotFound
|
|
758
|
+
# puts "No Such Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
759
|
+
rescue StandardError => e
|
|
760
|
+
handle_processing_error e
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# run a repair on a title or version
|
|
764
|
+
#
|
|
765
|
+
# @return [void]
|
|
766
|
+
###############################
|
|
767
|
+
def repair
|
|
768
|
+
if cli_cmd.version
|
|
769
|
+
conf_msg = "Repair Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
770
|
+
else
|
|
771
|
+
conf_msg = +"Repair Title '#{cli_cmd.title}'"
|
|
772
|
+
conf_msg << ' and all of its versions' if opts_to_process[:versions]
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
return unless confirmed?(conf_msg)
|
|
776
|
+
|
|
777
|
+
if cli_cmd.version
|
|
778
|
+
vers = Xolo::Admin::Version.fetch cli_cmd.title, cli_cmd.version, server_cnx
|
|
779
|
+
response_data = vers.repair server_cnx
|
|
780
|
+
else
|
|
781
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
782
|
+
response_data = title.repair server_cnx, versions: opts_to_process[:versions]
|
|
783
|
+
end
|
|
784
|
+
if debug?
|
|
785
|
+
puts "DEBUG: response_data: #{response_data}"
|
|
786
|
+
puts
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
display_progress response_data[:progress_stream_url_path]
|
|
790
|
+
rescue StandardError => e
|
|
791
|
+
handle_processing_error e
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Show the patch report for a title or version in xolo
|
|
795
|
+
#
|
|
796
|
+
# @return [void]
|
|
797
|
+
###############################
|
|
798
|
+
def patch_report
|
|
799
|
+
# since there is no actual version 'unknown'
|
|
800
|
+
# we'll fetch all of them and extract the onese we want
|
|
801
|
+
if cli_cmd.version == Xolo::UNKNOWN
|
|
802
|
+
cli_cmd.version = nil
|
|
803
|
+
report_unknown = true
|
|
804
|
+
else
|
|
805
|
+
report_unknown = false
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
if cli_cmd.version
|
|
809
|
+
vers = Xolo::Admin::Version.fetch cli_cmd.title, cli_cmd.version, server_cnx
|
|
810
|
+
data = vers.patch_report_data(server_cnx)
|
|
811
|
+
rpt_title = "Patch Report for Version '#{cli_cmd.version}' of Title '#{cli_cmd.title}'"
|
|
812
|
+
all_versions = false
|
|
813
|
+
else
|
|
814
|
+
title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
815
|
+
data = title.patch_report_data(server_cnx)
|
|
816
|
+
data.select! { |d| d[:version] == Xolo::UNKNOWN } if report_unknown
|
|
817
|
+
rpt_title = "Patch Report for Title '#{cli_cmd.title}'"
|
|
818
|
+
all_versions = true
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
if data.empty?
|
|
822
|
+
|
|
823
|
+
puts "# #{rpt_title}"
|
|
824
|
+
puts '# No Data Found'
|
|
825
|
+
|
|
826
|
+
return
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
if cli_cmd_opts.summary
|
|
830
|
+
display_patch_report_summary(data, rpt_title)
|
|
831
|
+
return
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
if json?
|
|
835
|
+
puts JSON.pretty_generate(data)
|
|
836
|
+
return
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# NOTE: The order of things in this array must match
|
|
840
|
+
# that in each array of the data, below
|
|
841
|
+
header_row = %w[Computer User]
|
|
842
|
+
header_row << 'Version' if all_versions
|
|
843
|
+
header_row << 'LastContact'
|
|
844
|
+
|
|
845
|
+
header_row << 'OS' if cli_cmd_opts.os
|
|
846
|
+
header_row << 'Dept' if cli_cmd_opts.dept
|
|
847
|
+
header_row << 'Building' if cli_cmd_opts.building
|
|
848
|
+
header_row << 'Site' if cli_cmd_opts.site
|
|
849
|
+
header_row << 'Frozen' if cli_cmd_opts.frozen
|
|
850
|
+
header_row << 'JamfID' if cli_cmd_opts.id
|
|
851
|
+
|
|
852
|
+
# See note above about the order of items in each sub-array
|
|
853
|
+
data = data.map do |d|
|
|
854
|
+
last_contact = Time.parse(d[:lastContactTime]).localtime.strftime('%F %T')
|
|
855
|
+
comp_ary = [d[:computerName], d[:username]]
|
|
856
|
+
comp_ary << d[:version] if all_versions
|
|
857
|
+
comp_ary << last_contact
|
|
858
|
+
|
|
859
|
+
comp_ary << d[:operatingSystemVersion] if cli_cmd_opts.os
|
|
860
|
+
comp_ary << d[:departmentName] if cli_cmd_opts.dept
|
|
861
|
+
comp_ary << d[:buildingName] if cli_cmd_opts.building
|
|
862
|
+
comp_ary << d[:siteName] if cli_cmd_opts.site
|
|
863
|
+
comp_ary << d[:frozen] if cli_cmd_opts.frozen
|
|
864
|
+
comp_ary << d[:deviceId] if cli_cmd_opts.id
|
|
865
|
+
comp_ary
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
show_text generate_report(data, header_row: header_row, title: rpt_title)
|
|
869
|
+
rescue StandardError => e
|
|
870
|
+
handle_processing_error e
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Show a summary of the patch report data
|
|
874
|
+
#
|
|
875
|
+
# @param data [Array<Hash>] the patch report data
|
|
876
|
+
# @return [void]
|
|
877
|
+
###############################
|
|
878
|
+
def display_patch_report_summary(data, rpt_title)
|
|
879
|
+
version_counts = {}
|
|
880
|
+
unknown = 0
|
|
881
|
+
data.each do |d|
|
|
882
|
+
if d[:version] == Xolo::UNKNOWN || d[:version].pix_empty?
|
|
883
|
+
unknown += 1
|
|
884
|
+
next
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
vers = Gem::Version.new(d[:version])
|
|
888
|
+
version_counts[vers] ||= 0
|
|
889
|
+
version_counts[vers] += 1
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
header_row = ['Version', 'Number of Installs']
|
|
893
|
+
title = "Summary #{rpt_title}"
|
|
894
|
+
|
|
895
|
+
summary_data = []
|
|
896
|
+
summary_data << ['All Versions', data.size] unless rpt_title.include? 'Version'
|
|
897
|
+
|
|
898
|
+
version_counts.keys.sort.reverse.each do |vers|
|
|
899
|
+
summary_data << [vers, version_counts[vers]]
|
|
900
|
+
end
|
|
901
|
+
summary_data << ['Unknown', unknown] if unknown.positive? && !rpt_title.include?('Version')
|
|
902
|
+
|
|
903
|
+
if json?
|
|
904
|
+
puts JSON.pretty_generate(summary_data.to_h)
|
|
905
|
+
return
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
show_text generate_report(summary_data, header_row: header_row, title: title)
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Show info about the server status
|
|
912
|
+
#
|
|
913
|
+
# @return [void]
|
|
914
|
+
###############################
|
|
915
|
+
def server_status
|
|
916
|
+
route = cli_cmd_opts.extended ? "#{SERVER_STATUS_ROUTE}?extended=true" : SERVER_STATUS_ROUTE
|
|
917
|
+
|
|
918
|
+
data = server_cnx.get(route).body
|
|
919
|
+
|
|
920
|
+
if json?
|
|
921
|
+
puts JSON.pretty_generate(data)
|
|
922
|
+
return
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
require 'pp'
|
|
926
|
+
|
|
927
|
+
header = +'# Xolo Server Status'
|
|
928
|
+
header << ' (extended)' if cli_cmd_opts.extended
|
|
929
|
+
puts header
|
|
930
|
+
puts '##################################################'
|
|
931
|
+
pp data
|
|
932
|
+
nil
|
|
933
|
+
rescue StandardError => e
|
|
934
|
+
handle_processing_error e
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# kick off server cleanup
|
|
938
|
+
#
|
|
939
|
+
# @return [void]
|
|
940
|
+
###############################
|
|
941
|
+
def server_cleanup
|
|
942
|
+
return unless confirmed? 'Run the Xolo Server cleanup process'
|
|
943
|
+
|
|
944
|
+
result = server_cnx.post(SERVER_CLENUP_ROUTE).body
|
|
945
|
+
puts result[:result]
|
|
946
|
+
rescue StandardError => e
|
|
947
|
+
handle_processing_error e
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
# force update the client data pkg
|
|
951
|
+
#
|
|
952
|
+
# @return [void]
|
|
953
|
+
###############################
|
|
954
|
+
def update_client_data
|
|
955
|
+
return unless confirmed? 'Force update of the client data package'
|
|
956
|
+
|
|
957
|
+
result = server_cnx.post(SERVER_UPDATE_CLIENT_DATA_ROUTE).body
|
|
958
|
+
puts result[:result]
|
|
959
|
+
rescue StandardError => e
|
|
960
|
+
handle_processing_error e
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# rotate the server logs
|
|
964
|
+
#
|
|
965
|
+
# @return [void]
|
|
966
|
+
###############################
|
|
967
|
+
def rotate_server_logs
|
|
968
|
+
return unless confirmed? 'Rotate the Xolo Server logs'
|
|
969
|
+
|
|
970
|
+
result = server_cnx.post(SERVER_ROTATE_LOGS_ROUTE).body
|
|
971
|
+
puts result[:result]
|
|
972
|
+
rescue StandardError => e
|
|
973
|
+
handle_processing_error e
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# set the server log level
|
|
977
|
+
#
|
|
978
|
+
# @return [void]
|
|
979
|
+
###############################
|
|
980
|
+
def set_server_log_level
|
|
981
|
+
level = ARGV.shift&.downcase
|
|
982
|
+
raise ArgumentError, 'No log level given' unless level
|
|
983
|
+
|
|
984
|
+
unless LOG_LEVELS.include? level
|
|
985
|
+
raise ArgumentError, "Invalid log level '#{level}', must be one of #{LOG_LEVELS.join(', ')}"
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
return unless confirmed? "Set the Xolo Server log level to '#{level}'?"
|
|
989
|
+
|
|
990
|
+
payload = { level: level }
|
|
991
|
+
result = server_cnx.post(SERVER_LOG_LEVEL_ROUTE, payload).body
|
|
992
|
+
puts result[:result]
|
|
993
|
+
rescue StandardError => e
|
|
994
|
+
handle_processing_error e
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
# shutdown the server
|
|
998
|
+
#
|
|
999
|
+
# @return [void]
|
|
1000
|
+
###############################
|
|
1001
|
+
def shutdown_server
|
|
1002
|
+
return unless confirmed? 'Shutdown the Xolo Server'
|
|
1003
|
+
|
|
1004
|
+
restart = cli_cmd_opts.restart
|
|
1005
|
+
action = restart ? 'Restart' : 'Shutdown'
|
|
1006
|
+
payload = { restart: restart }
|
|
1007
|
+
|
|
1008
|
+
result = server_cnx.post(SERVER_SHUTDOWN_ROUTE, payload).body
|
|
1009
|
+
puts "result[:result] = #{result[:result]}"
|
|
1010
|
+
|
|
1011
|
+
if debug?
|
|
1012
|
+
puts "DEBUG: response_data: #{result}"
|
|
1013
|
+
puts
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
display_progress result[:progress_stream_url_path]
|
|
1017
|
+
|
|
1018
|
+
puts "#{action} complete"
|
|
1019
|
+
rescue Faraday::ConnectionFailed
|
|
1020
|
+
puts "#{action} complete"
|
|
1021
|
+
rescue StandardError => e
|
|
1022
|
+
puts "Error Class: #{e.class}"
|
|
1023
|
+
handle_processing_error e
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
# List all the computer groups in jamf pro
|
|
1027
|
+
#
|
|
1028
|
+
# @return [void]
|
|
1029
|
+
############################
|
|
1030
|
+
def list_groups
|
|
1031
|
+
if json?
|
|
1032
|
+
puts JSON.pretty_generate(jamf_computer_group_names)
|
|
1033
|
+
return
|
|
1034
|
+
end
|
|
1035
|
+
header = "Computer Groups in Jamf Pro.\n# Those starting with 'xolo-' are used internally by Xolo and not shown."
|
|
1036
|
+
list_in_cols header, jamf_computer_group_names.sort_by(&:downcase)
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# List all the SSVC categories in jamf pro
|
|
1040
|
+
#
|
|
1041
|
+
# @return [void]
|
|
1042
|
+
############################
|
|
1043
|
+
def list_categories
|
|
1044
|
+
if json?
|
|
1045
|
+
puts JSON.pretty_generate(jamf_category_names)
|
|
1046
|
+
return
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
list_in_cols 'Categories in Jamf Pro:', jamf_category_names.sort_by(&:downcase)
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
# Save the xolo client tool from the gem's data dir to a
|
|
1053
|
+
# desired dir or /tmp
|
|
1054
|
+
#
|
|
1055
|
+
# @return [void]
|
|
1056
|
+
##########################
|
|
1057
|
+
def save_client_code
|
|
1058
|
+
dest_dir = ARGV.shift
|
|
1059
|
+
dest_dir ||= '/tmp'
|
|
1060
|
+
dest_dir = Pathname.new(dest_dir).expand_path
|
|
1061
|
+
unless dest_dir.directory? && dest_dir.writable?
|
|
1062
|
+
raise ArgumentError,
|
|
1063
|
+
"Destination directory '#{dest_dir}' does not exist or is not writable"
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
this_file = Pathname.new(__FILE__).expand_path
|
|
1067
|
+
# This file is .../gems/xolo-admin-<vers>/lib/xolo/admin/processing.rb
|
|
1068
|
+
# so....
|
|
1069
|
+
# parent1 = admin
|
|
1070
|
+
# parent2 - xolo
|
|
1071
|
+
# parent3 = lib
|
|
1072
|
+
# parent4 = xolo-admin-<vers>
|
|
1073
|
+
# and we want xolo-admin-<vers>/data/client/xolo
|
|
1074
|
+
src = this_file.parent.parent.parent.parent + 'data/client/xolo'
|
|
1075
|
+
dest = dest_dir + 'xolo'
|
|
1076
|
+
|
|
1077
|
+
src.pix_cp dest
|
|
1078
|
+
puts "Saved 'xolo' client tool to '#{dest}'"
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
# run the cleanup
|
|
1082
|
+
# get the /test route to do whatever testing it does
|
|
1083
|
+
# during testing - this will return all kinds of things.
|
|
1084
|
+
#
|
|
1085
|
+
# @return [void]
|
|
1086
|
+
##########################
|
|
1087
|
+
def run_test_route
|
|
1088
|
+
cli_cmd.command = 'test'
|
|
1089
|
+
if ARGV.include? '--quiet'
|
|
1090
|
+
global_opts.quiet = true
|
|
1091
|
+
puts "Set global_opts.quiet = true : #{global_opts.quiet}"
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
login test: true
|
|
1095
|
+
resp = server_cnx.get('/default_min_os').body.first.to_s
|
|
1096
|
+
puts "RESPONSE:\n#{resp}"
|
|
1097
|
+
return unless resp[:progress_stream_url_path]
|
|
1098
|
+
|
|
1099
|
+
puts
|
|
1100
|
+
if global_opts.quiet
|
|
1101
|
+
puts 'given --quiet, not showing progress'
|
|
1102
|
+
else
|
|
1103
|
+
puts 'Streaming progress:'
|
|
1104
|
+
display_progress resp[:progress_stream_url_path]
|
|
1105
|
+
end
|
|
1106
|
+
|
|
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
|
|
1112
|
+
puts "Uploading Test File #{pkg_to_upload.size} bytes... "
|
|
1113
|
+
upload_test_file(pkg_to_upload)
|
|
1114
|
+
|
|
1115
|
+
puts 'All Done!'
|
|
1116
|
+
rescue StandardError => e
|
|
1117
|
+
msg = e.respond_to?(:response_body) ? "#{e}\nRespBody: #{e.response_body}" : e.to_s
|
|
1118
|
+
puts "TEST ERROR: #{e.class}: #{msg}"
|
|
1119
|
+
puts e.backtrace
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
# Upload a file to the test upload route
|
|
1123
|
+
#
|
|
1124
|
+
# @param pkg_to_upload [Pathname] a local file to upload
|
|
1125
|
+
#
|
|
1126
|
+
# @return [void]
|
|
1127
|
+
#
|
|
1128
|
+
############################
|
|
1129
|
+
def upload_test_file(pkg_to_upload)
|
|
1130
|
+
route = '/upload/test'
|
|
1131
|
+
|
|
1132
|
+
upfile = Faraday::UploadIO.new(
|
|
1133
|
+
pkg_to_upload.to_s,
|
|
1134
|
+
'application/octet-stream',
|
|
1135
|
+
pkg_to_upload.basename.to_s
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
content = { file: upfile }
|
|
1139
|
+
# upload the file in a thread
|
|
1140
|
+
Thread.new { upload_cnx.post(route) { |req| req.body = content } }
|
|
1141
|
+
|
|
1142
|
+
# when the server starts the upload, it notes the new
|
|
1143
|
+
# streaming url for our session[:xolo_id], which we can then fetch and
|
|
1144
|
+
# start displaying the progress
|
|
1145
|
+
display_progress response_data[:progress_stream_url_path]
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# get confirmation for an action that requires it
|
|
1149
|
+
# @param action [String] A short description of what we're about to do,
|
|
1150
|
+
# e.g. "Add version '1.2.3' to title 'cool-app'"
|
|
1151
|
+
#
|
|
1152
|
+
# @return [Boolean] did we get confirmation?
|
|
1153
|
+
############################
|
|
1154
|
+
def confirmed?(action)
|
|
1155
|
+
return true unless need_confirmation?
|
|
1156
|
+
|
|
1157
|
+
puts "About to: #{action}"
|
|
1158
|
+
print 'Are you sure? (y/n): '
|
|
1159
|
+
STDIN.gets.chomp.downcase.start_with? 'y'
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
# Start displaying the progress of a long-running task on the server
|
|
1163
|
+
# but only if we aren't --quiet
|
|
1164
|
+
# @return [void]
|
|
1165
|
+
##################################
|
|
1166
|
+
def display_progress(url_path)
|
|
1167
|
+
# in case it's nil
|
|
1168
|
+
return unless url_path
|
|
1169
|
+
|
|
1170
|
+
# always make note of the path in the history
|
|
1171
|
+
add_progress_history_entry url_path
|
|
1172
|
+
return if quiet?
|
|
1173
|
+
|
|
1174
|
+
# if any line of the contains 'ERROR' we can skip
|
|
1175
|
+
# any post-stream processing.
|
|
1176
|
+
@streaming_error = false
|
|
1177
|
+
|
|
1178
|
+
streaming_cnx.get url_path
|
|
1179
|
+
|
|
1180
|
+
raise Xolo::ServerError, 'There was an error while streaming the server progress.' if @streaming_error
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
# Handle errors while processing xadm commands
|
|
1184
|
+
#
|
|
1185
|
+
#######################
|
|
1186
|
+
def handle_processing_error(err)
|
|
1187
|
+
# puts "Err: #{err.class} #{err}"
|
|
1188
|
+
case err
|
|
1189
|
+
when Faraday::Error
|
|
1190
|
+
begin
|
|
1191
|
+
jsonerr = parse_json err.response_body
|
|
1192
|
+
errmsg = "#{jsonerr[:error]} [#{err.response_status}]"
|
|
1193
|
+
|
|
1194
|
+
# if we got a faraday error, but it didn't contain
|
|
1195
|
+
# JSON, return just the error body, or the error itself
|
|
1196
|
+
rescue StandardError
|
|
1197
|
+
msg = err.response_body if err.respond_to?(:response_body)
|
|
1198
|
+
msg ||= err.to_s
|
|
1199
|
+
errmsg = "#{err.class.name.split('::').last}: #{msg}"
|
|
1200
|
+
end # begin
|
|
1201
|
+
raise err.class, errmsg
|
|
1202
|
+
|
|
1203
|
+
else
|
|
1204
|
+
raise err
|
|
1205
|
+
end # case
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
# Just output lots of local things, for testing
|
|
1209
|
+
#
|
|
1210
|
+
# Comment/uncomment as needed
|
|
1211
|
+
#
|
|
1212
|
+
########################
|
|
1213
|
+
def do_local_testing
|
|
1214
|
+
puts '-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+'
|
|
1215
|
+
puts Xolo::Admin::Title.release_to_all_allowed?(server_cnx)
|
|
1216
|
+
|
|
1217
|
+
###################
|
|
1218
|
+
# puts
|
|
1219
|
+
# puts '-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+'
|
|
1220
|
+
# puts 'GLOBAL OPTS:'
|
|
1221
|
+
# puts '----'
|
|
1222
|
+
# global_opts.to_h.each do |k, v|
|
|
1223
|
+
# puts "..#{k} => #{v}"
|
|
1224
|
+
# end
|
|
1225
|
+
|
|
1226
|
+
###################
|
|
1227
|
+
# puts
|
|
1228
|
+
# puts "COMMAND: #{cli_cmd.command}"
|
|
1229
|
+
|
|
1230
|
+
###################
|
|
1231
|
+
# puts
|
|
1232
|
+
# puts "TITLE: #{cli_cmd.title}"
|
|
1233
|
+
|
|
1234
|
+
###################
|
|
1235
|
+
# puts
|
|
1236
|
+
# puts "VERSION: #{cli_cmd.version}"
|
|
1237
|
+
|
|
1238
|
+
###################
|
|
1239
|
+
# puts
|
|
1240
|
+
# puts 'CURRENT OPT VALUES:'
|
|
1241
|
+
# puts 'The values the object had before xadm started working on it.'
|
|
1242
|
+
# puts 'If the object is being added, these are the default or inherited values'
|
|
1243
|
+
# puts '----'
|
|
1244
|
+
# current_opt_values.to_h.each do |k, v|
|
|
1245
|
+
# puts "..#{k} => #{v}"
|
|
1246
|
+
# end
|
|
1247
|
+
|
|
1248
|
+
###################
|
|
1249
|
+
# puts
|
|
1250
|
+
# puts 'COMMAND OPT VALUES:'
|
|
1251
|
+
# puts 'The command options collected by xadm, merged with the'
|
|
1252
|
+
# puts 'current values, to be applied to the object'
|
|
1253
|
+
# puts '----'
|
|
1254
|
+
# opts = walkthru? ? walkthru_cmd_opts : cli_cmd_opts
|
|
1255
|
+
# opts.to_h.each do |k, v|
|
|
1256
|
+
# puts "..#{k} => #{v}"
|
|
1257
|
+
# end
|
|
1258
|
+
|
|
1259
|
+
###################
|
|
1260
|
+
# puts 'CookieJar:'
|
|
1261
|
+
# puts " Session: #{Xolo::Admin::CookieJar.session_cookie}"
|
|
1262
|
+
# puts " Expires: #{Xolo::Admin::CookieJar.session_expires}"
|
|
1263
|
+
|
|
1264
|
+
###################
|
|
1265
|
+
# puts 'getting /state'
|
|
1266
|
+
# resp = server_cnx.get '/state'
|
|
1267
|
+
# puts "#{resp.body}"
|
|
1268
|
+
|
|
1269
|
+
# ###################
|
|
1270
|
+
# puts 'Listing currently known titles:'
|
|
1271
|
+
# all_titles = Xolo::Admin::Title.all_titles server_cnx
|
|
1272
|
+
# puts all_titles
|
|
1273
|
+
|
|
1274
|
+
# # ###############
|
|
1275
|
+
# already_there = all_titles.include? cli_cmd.title
|
|
1276
|
+
# puts "all titles contains our title: #{already_there}"
|
|
1277
|
+
# if already_there
|
|
1278
|
+
# puts 'deleting the title first'
|
|
1279
|
+
# resp = Xolo::Admin::Title.delete cli_cmd.title, server_cnx
|
|
1280
|
+
# puts "Delete Status: #{resp.status}"
|
|
1281
|
+
# puts 'Delete Body:'
|
|
1282
|
+
# puts resp.body
|
|
1283
|
+
# end
|
|
1284
|
+
|
|
1285
|
+
# # ###################
|
|
1286
|
+
# process_method = Xolo::Admin::Options::COMMANDS[cli_cmd.command][:process_method]
|
|
1287
|
+
# puts
|
|
1288
|
+
# puts "Processing command opts using method: #{process_method}"
|
|
1289
|
+
# resp = send process_method if process_method
|
|
1290
|
+
# puts "Add Status: #{resp.status}"
|
|
1291
|
+
# puts 'Add Body:'
|
|
1292
|
+
# puts resp.body
|
|
1293
|
+
# puts
|
|
1294
|
+
|
|
1295
|
+
# ##################
|
|
1296
|
+
# puts 're-fetching...'
|
|
1297
|
+
# title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
|
|
1298
|
+
# puts "title class: #{title.class}"
|
|
1299
|
+
# puts 'title to_h:'
|
|
1300
|
+
# puts title.to_h
|
|
1301
|
+
# puts
|
|
1302
|
+
|
|
1303
|
+
# ##################
|
|
1304
|
+
# puts 'updating...'
|
|
1305
|
+
# title.self_service = false
|
|
1306
|
+
# resp = title.update server_cnx
|
|
1307
|
+
# puts "Update Status: #{resp.status}"
|
|
1308
|
+
# puts 'Update Body:'
|
|
1309
|
+
# puts resp.body
|
|
1310
|
+
# puts
|
|
1311
|
+
|
|
1312
|
+
###################
|
|
1313
|
+
# puts 'running jamf_package_names'
|
|
1314
|
+
# puts jamf_package_names
|
|
1315
|
+
|
|
1316
|
+
###################
|
|
1317
|
+
# puts 'running ted_titles'
|
|
1318
|
+
# puts ted_titles
|
|
1319
|
+
|
|
1320
|
+
##################
|
|
1321
|
+
# puts
|
|
1322
|
+
# puts 'DONE'
|
|
1323
|
+
end # do_local_testing
|
|
1324
|
+
|
|
1325
|
+
end # module processing
|
|
1326
|
+
|
|
1327
|
+
end # module Admin
|
|
1328
|
+
|
|
1329
|
+
end # module Xolo
|