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.
@@ -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