xolo-admin 1.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -3
- data/bin/xadm +4 -1
- data/data/client/xolo +1233 -0
- data/lib/optimist_with_insert_blanks.rb +1216 -0
- data/lib/xolo/admin/command_line.rb +18 -5
- data/lib/xolo/admin/configuration.rb +16 -0
- data/lib/xolo/admin/connection.rb +18 -5
- data/lib/xolo/admin/credentials.rb +94 -50
- data/lib/xolo/admin/highline_terminal.rb +14 -47
- data/lib/xolo/admin/interactive.rb +146 -15
- data/lib/xolo/admin/jamf_pro.rb +14 -0
- data/lib/xolo/admin/options.rb +37 -3
- data/lib/xolo/admin/processing.rb +78 -13
- data/lib/xolo/admin/progress_history.rb +1 -1
- data/lib/xolo/admin/title.rb +33 -24
- data/lib/xolo/admin/validate.rb +222 -44
- data/lib/xolo/admin/version.rb +55 -18
- data/lib/xolo/core/base_classes/configuration.rb +238 -0
- data/lib/xolo/core/base_classes/server_object.rb +112 -0
- data/lib/xolo/core/base_classes/title.rb +884 -0
- data/lib/xolo/core/base_classes/version.rb +641 -0
- data/lib/xolo/core/constants.rb +85 -0
- data/lib/xolo/core/exceptions.rb +52 -0
- data/lib/xolo/core/json_wrappers.rb +43 -0
- data/lib/xolo/core/loading.rb +59 -0
- data/lib/xolo/core/output.rb +292 -0
- data/lib/xolo/core/security_cmd.rb +128 -0
- data/lib/xolo/core/version.rb +21 -0
- data/lib/xolo/core.rb +47 -0
- metadata +17 -11
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
# frozen_string_literal: true
|
|
7
|
+
|
|
8
|
+
module Xolo
|
|
9
|
+
|
|
10
|
+
module Core
|
|
11
|
+
|
|
12
|
+
# Constants useful throughout Xolo
|
|
13
|
+
#####################################
|
|
14
|
+
module Constants
|
|
15
|
+
|
|
16
|
+
# Empty strings are used in various places
|
|
17
|
+
BLANK = ''
|
|
18
|
+
|
|
19
|
+
# The value to use when unsetting an option
|
|
20
|
+
NONE = 'none'
|
|
21
|
+
|
|
22
|
+
OK = 'OK'
|
|
23
|
+
|
|
24
|
+
ERROR = 'ERROR'
|
|
25
|
+
|
|
26
|
+
# Several things use x
|
|
27
|
+
X = 'x'
|
|
28
|
+
|
|
29
|
+
# CLI options and other things use dashes
|
|
30
|
+
DASH = '-'
|
|
31
|
+
|
|
32
|
+
# Several things use dots
|
|
33
|
+
DOT = '.'
|
|
34
|
+
|
|
35
|
+
# Cancelling is often an option
|
|
36
|
+
CANCEL = 'Cancel'
|
|
37
|
+
|
|
38
|
+
# and we check for things ending with .app
|
|
39
|
+
DOTAPP = '.app'
|
|
40
|
+
|
|
41
|
+
DOTJSON = '.json'
|
|
42
|
+
|
|
43
|
+
# These are handy for testing values without making new arrays, strings, etc every time.
|
|
44
|
+
TRUE_FALSE = [true, false].freeze
|
|
45
|
+
|
|
46
|
+
# lots of things get split on commmas
|
|
47
|
+
COMMA_SEP_RE = /\s*,\s*/.freeze
|
|
48
|
+
|
|
49
|
+
# lots of things get joined with commas
|
|
50
|
+
COMMA_JOIN = ', '
|
|
51
|
+
|
|
52
|
+
# Some things get split on semicolons
|
|
53
|
+
SEMICOLON_SEP_RE = /\s*;\s*/.freeze
|
|
54
|
+
|
|
55
|
+
# Once a thing has been uploaded and saved, this
|
|
56
|
+
# is what the server returns as the attr value
|
|
57
|
+
ITEM_UPLOADED = 'uploaded'
|
|
58
|
+
|
|
59
|
+
DOT_PKG = '.pkg'
|
|
60
|
+
|
|
61
|
+
UNKNOWN = 'unknown'
|
|
62
|
+
|
|
63
|
+
# Installer packages must have one of these extensions
|
|
64
|
+
OK_PKG_EXTS = [DOT_PKG]
|
|
65
|
+
|
|
66
|
+
# The value to use when all computers are the release-targets
|
|
67
|
+
# and for all manual-install policies
|
|
68
|
+
TARGET_ALL = 'all'
|
|
69
|
+
|
|
70
|
+
# Title Types
|
|
71
|
+
MANAGED = :managed
|
|
72
|
+
SUBSCRIBED = :subscribed
|
|
73
|
+
TITLE_TYPES = [MANAGED, SUBSCRIBED].freeze
|
|
74
|
+
DEFAULT_TITLE_TYPE = :managed
|
|
75
|
+
|
|
76
|
+
# when this module is included
|
|
77
|
+
def self.included(includer)
|
|
78
|
+
Xolo.verbose_include includer, self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end # module constants
|
|
82
|
+
|
|
83
|
+
end # module core
|
|
84
|
+
|
|
85
|
+
end # module Xolo
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Xolo
|
|
9
|
+
|
|
10
|
+
module Core
|
|
11
|
+
|
|
12
|
+
module Exceptions
|
|
13
|
+
|
|
14
|
+
# General errors
|
|
15
|
+
|
|
16
|
+
class MissingDataError < RuntimeError; end
|
|
17
|
+
|
|
18
|
+
class InvalidDataError < RuntimeError; end
|
|
19
|
+
|
|
20
|
+
class NoSuchItemError < RuntimeError; end
|
|
21
|
+
|
|
22
|
+
class UnsupportedError < RuntimeError; end
|
|
23
|
+
|
|
24
|
+
class KeychainError < RuntimeError; end
|
|
25
|
+
|
|
26
|
+
class ActionRequiredError < RuntimeError; end
|
|
27
|
+
|
|
28
|
+
# Connections & Access
|
|
29
|
+
|
|
30
|
+
class ConnectionError < RuntimeError; end
|
|
31
|
+
|
|
32
|
+
class NotConnectedError < ConnectionError; end
|
|
33
|
+
|
|
34
|
+
class TimeoutError < ConnectionError; end
|
|
35
|
+
|
|
36
|
+
class AuthenticationError < ConnectionError; end
|
|
37
|
+
|
|
38
|
+
class PermissionError < ConnectionError; end
|
|
39
|
+
|
|
40
|
+
class InvalidTokenError < ConnectionError; end
|
|
41
|
+
|
|
42
|
+
class ServerError < ConnectionError; end
|
|
43
|
+
|
|
44
|
+
# Parsing errors
|
|
45
|
+
|
|
46
|
+
class DisallowedYAMLDumpClass; end
|
|
47
|
+
|
|
48
|
+
end # module Exceptions
|
|
49
|
+
|
|
50
|
+
end # module Core
|
|
51
|
+
|
|
52
|
+
end # module Xolo
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Core
|
|
14
|
+
|
|
15
|
+
# constants and methods for consistent JSON processing on the server
|
|
16
|
+
module JSONWrappers
|
|
17
|
+
|
|
18
|
+
# when this module is extended
|
|
19
|
+
def self.extended(extender)
|
|
20
|
+
Xolo.verbose_extend extender, self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# when this module is included
|
|
24
|
+
def self.included(includer)
|
|
25
|
+
Xolo.verbose_include includer, self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A wrapper for JSON.parse that always uses :symbolize_names
|
|
29
|
+
# def self.parse_json(str)
|
|
30
|
+
# JSON.parse str, symbolize_names: true
|
|
31
|
+
# end
|
|
32
|
+
|
|
33
|
+
# A wrapper for JSON.parse that always uses :symbolize_names
|
|
34
|
+
# and ensures UTF-8 encoding
|
|
35
|
+
def parse_json(str)
|
|
36
|
+
JSON.parse str.force_encoding('UTF-8'), symbolize_names: true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end # JSON
|
|
40
|
+
|
|
41
|
+
end # Core
|
|
42
|
+
|
|
43
|
+
end # module Xolo
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
module Xolo
|
|
10
|
+
|
|
11
|
+
module Core
|
|
12
|
+
|
|
13
|
+
module Loading
|
|
14
|
+
|
|
15
|
+
# touch this file to make mixins send text to stderr as things load
|
|
16
|
+
# or get mixed in
|
|
17
|
+
VERBOSE_LOADING_FILE = Pathname.new('/tmp/xolo-verbose-loading')
|
|
18
|
+
|
|
19
|
+
# Or, set this ENV var to also make mixins send text to stderr
|
|
20
|
+
VERBOSE_LOADING_ENV = 'XOLO_VERBOSE_LOADING'
|
|
21
|
+
|
|
22
|
+
def self.extended(extender)
|
|
23
|
+
Xolo.verbose_extend extender, self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Only look at the filesystem once.
|
|
27
|
+
def verbose_loading?
|
|
28
|
+
return @verbose_loading unless @verbose_loading.nil?
|
|
29
|
+
|
|
30
|
+
@verbose_loading = VERBOSE_LOADING_FILE.file?
|
|
31
|
+
@verbose_loading ||= ENV.include? VERBOSE_LOADING_ENV
|
|
32
|
+
@verbose_loading
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Send a message to stderr if verbose loading is enabled
|
|
36
|
+
def load_msg(msg)
|
|
37
|
+
warn msg if verbose_loading?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Mention that a module is being included into something
|
|
41
|
+
def verbose_include(includer, includee)
|
|
42
|
+
load_msg "--> #{includer} is including #{includee}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Mention that a module is being extended into something
|
|
46
|
+
def verbose_extend(extender, extendee)
|
|
47
|
+
load_msg "--> #{extender} is extending #{extendee}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Mention that a module is being extended into something
|
|
51
|
+
def verbose_inherit(child_class, parent_class)
|
|
52
|
+
load_msg "--> #{child_class} is a Subclass inheriting from #{parent_class}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end # module
|
|
56
|
+
|
|
57
|
+
end # module
|
|
58
|
+
|
|
59
|
+
end # module Xolo
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require 'io/console'
|
|
10
|
+
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Core
|
|
14
|
+
|
|
15
|
+
# Methods for formattng and sending output to stdout
|
|
16
|
+
# Should be included in classes as needed
|
|
17
|
+
#
|
|
18
|
+
# NOTE: Help output is auto-generated by 'optimist'
|
|
19
|
+
# The methods here are mostly for presenting info like
|
|
20
|
+
# columnizd lists and reports and the like.
|
|
21
|
+
module Output
|
|
22
|
+
|
|
23
|
+
# Constants
|
|
24
|
+
#############################
|
|
25
|
+
#############################
|
|
26
|
+
|
|
27
|
+
# This is used when we are not outputting to a terminal
|
|
28
|
+
# usually we're being piped or not running in a terminal
|
|
29
|
+
# so the lines should be as long as they want
|
|
30
|
+
DEFAULT_LINE_WIDTH = 2000
|
|
31
|
+
|
|
32
|
+
# Module methods
|
|
33
|
+
#
|
|
34
|
+
# These are available as module methods but not as 'helper'
|
|
35
|
+
# methods in sinatra routes & views.
|
|
36
|
+
#
|
|
37
|
+
##############################
|
|
38
|
+
##############################
|
|
39
|
+
|
|
40
|
+
# when this module is included
|
|
41
|
+
##############################
|
|
42
|
+
def self.included(includer)
|
|
43
|
+
Xolo.verbose_include includer, self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# when this module is extended
|
|
47
|
+
def self.extended(extender)
|
|
48
|
+
Xolo.verbose_extend extender, self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Instance methods
|
|
52
|
+
#
|
|
53
|
+
# These are available directly in sinatra routes and views
|
|
54
|
+
#
|
|
55
|
+
##############################
|
|
56
|
+
##############################
|
|
57
|
+
|
|
58
|
+
# @return [Integer] how many rows high is our terminal?
|
|
59
|
+
#########################
|
|
60
|
+
def terminal_height
|
|
61
|
+
IO.console.winsize.first
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer] how many columns wide is our terminal?
|
|
65
|
+
#########################
|
|
66
|
+
def terminal_width
|
|
67
|
+
IO.console.winsize.last
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Integer] how wide is our word wrap? terminal-width minus 5
|
|
71
|
+
#########################
|
|
72
|
+
def terminal_word_wrap
|
|
73
|
+
@terminal_word_wrap ||= terminal_width - 5
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# format a multi-line value by prepending the desired indentation
|
|
77
|
+
# to all but the first line, which is expected to be indented in-place where
|
|
78
|
+
# its being used.
|
|
79
|
+
#
|
|
80
|
+
# @param value [String] the value to format
|
|
81
|
+
# @param indent [Integer] the number of spaces to indent all but the first line
|
|
82
|
+
# @return [String] the formatted value
|
|
83
|
+
#######################
|
|
84
|
+
def format_multiline_indent(value, indent:)
|
|
85
|
+
value = value.to_s
|
|
86
|
+
return value unless value.include? "\n"
|
|
87
|
+
|
|
88
|
+
lines = value.split("\n")
|
|
89
|
+
lines[1..-1].each { |line| line.prepend ' ' * indent }
|
|
90
|
+
lines.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# TODO: Move this out of Xolo::Core
|
|
94
|
+
# Display a list of items in as many columns as possible
|
|
95
|
+
# based on terminal width, e.g. with 3 cols:
|
|
96
|
+
#
|
|
97
|
+
# a thing another thing third thing
|
|
98
|
+
# thing2 line2 thing third line 2
|
|
99
|
+
# line3 thing another one and yet a 3rd
|
|
100
|
+
# oh my line4 4 line ok? yes, this is it
|
|
101
|
+
#
|
|
102
|
+
# and if the list is longer than terminal height,
|
|
103
|
+
# pipe it through 'less'
|
|
104
|
+
#
|
|
105
|
+
# @param header [String] A string to display at the top prepended with a '#'
|
|
106
|
+
# and appended with a newline and a line of ######'s of the same length.
|
|
107
|
+
#
|
|
108
|
+
# @param list [Array<String>] the items to list
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
############################
|
|
112
|
+
def list_in_cols(header, list)
|
|
113
|
+
longest_list_item = list.map(&:size).max
|
|
114
|
+
use_columns = (longest_list_item + 5) < terminal_width
|
|
115
|
+
|
|
116
|
+
list_to_display = use_columns ? highline_cli.list(list, :columns_down) : list.join("\n")
|
|
117
|
+
|
|
118
|
+
output = +"# #{header}\n"
|
|
119
|
+
output << '#' * (terminal_width - 5)
|
|
120
|
+
output << "\n"
|
|
121
|
+
output << list_to_display
|
|
122
|
+
|
|
123
|
+
show_text output
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Generate a report of rowed/columned data, either fixed-width or tab-delimited.
|
|
127
|
+
#
|
|
128
|
+
# Title and header lines are pre-pended with '# ' for easier exclusion
|
|
129
|
+
# when using the report as input for some other program.
|
|
130
|
+
# If the :type is :fixed, so will the column header line.
|
|
131
|
+
# (however, for parsing this data, try using the --json option)
|
|
132
|
+
#
|
|
133
|
+
# @param lines [Array<Array>] the rows and columns of data
|
|
134
|
+
#
|
|
135
|
+
# @param type [Symbol] :fixed or :tab, defaults to :fixed
|
|
136
|
+
#
|
|
137
|
+
# @params title [String] a descriptive text or title, shown above the
|
|
138
|
+
# column headers. Every line is pre-pended with '# '.
|
|
139
|
+
# Only used on :fixed reports.
|
|
140
|
+
#
|
|
141
|
+
# @params header_row [Array<String>] the column headers. optional.
|
|
142
|
+
#
|
|
143
|
+
# @return [String] the formatted report.
|
|
144
|
+
#
|
|
145
|
+
############################
|
|
146
|
+
def generate_report(lines, type: :fixed, header_row: [], title: nil)
|
|
147
|
+
return Xolo::BLANK if lines.pix_empty?
|
|
148
|
+
|
|
149
|
+
raise ArgumentError, 'The first argument must be an Array' unless lines.is_a?(Array)
|
|
150
|
+
raise ArgumentError, 'The header_row must be an Array' unless header_row.is_a? Array
|
|
151
|
+
|
|
152
|
+
# tab delim is easy
|
|
153
|
+
if type == :tab
|
|
154
|
+
report_tab = header_row.join("\t")
|
|
155
|
+
lines.each { |line| report_tab += "\n#{line.join("\t")}" }
|
|
156
|
+
return report_tab.strip
|
|
157
|
+
end # if :tab
|
|
158
|
+
|
|
159
|
+
# below here, fixed width
|
|
160
|
+
|
|
161
|
+
line_width, format_str = width_and_format(lines, header_row)
|
|
162
|
+
|
|
163
|
+
# title if given
|
|
164
|
+
report = title ? +"# #{title}\n" : +''
|
|
165
|
+
|
|
166
|
+
unless header_row.empty?
|
|
167
|
+
unless header_row.size == lines[0].size
|
|
168
|
+
raise ArgumentError,
|
|
169
|
+
"Header row must have #{lines[0].count} items"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# then the header line if given
|
|
173
|
+
report += format_str % header_row
|
|
174
|
+
# add a separator
|
|
175
|
+
report += '#' + ('-' * (line_width - 1)) + "\n"
|
|
176
|
+
end
|
|
177
|
+
# add the rows
|
|
178
|
+
lines.each { |line| report += format_str % line }
|
|
179
|
+
|
|
180
|
+
report
|
|
181
|
+
end # generate report
|
|
182
|
+
|
|
183
|
+
# Given an Array of Arrays representing rows and columns of data,
|
|
184
|
+
# figure out the appropriate line-width for the longest line
|
|
185
|
+
# and the printf format string to create the columns
|
|
186
|
+
#
|
|
187
|
+
#
|
|
188
|
+
# @param lines [Array<Array>] The rows and columns of data
|
|
189
|
+
#
|
|
190
|
+
# @param header_row [Array] An optional header row to include in the
|
|
191
|
+
# width calculation.
|
|
192
|
+
#
|
|
193
|
+
# @return [Array<Integer, String>] the line width and format string
|
|
194
|
+
#
|
|
195
|
+
############################
|
|
196
|
+
def width_and_format(lines, header_row = [])
|
|
197
|
+
# below here, fixed width
|
|
198
|
+
format_str = +''
|
|
199
|
+
line_width = 0
|
|
200
|
+
header_row[0] = "# #{header_row[0]}"
|
|
201
|
+
|
|
202
|
+
col_widths(lines, header_row).each do |w|
|
|
203
|
+
# make sure there's a space between columns
|
|
204
|
+
col_width = w + 1
|
|
205
|
+
|
|
206
|
+
# add the column to the printf format
|
|
207
|
+
format_str += "%-#{col_width}s"
|
|
208
|
+
line_width += col_width
|
|
209
|
+
end
|
|
210
|
+
format_str += "\n"
|
|
211
|
+
|
|
212
|
+
# if needed, limit the total line width for the header the width of the terminal
|
|
213
|
+
max_width = $stdout.tty? ? terminal_word_wrap : DEFAULT_LINE_WIDTH
|
|
214
|
+
line_width = max_width if line_width > max_width
|
|
215
|
+
|
|
216
|
+
[line_width, format_str]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Given an Array of Arrays representing rows and columns of data
|
|
220
|
+
# figure out the widest width of each column and return an array
|
|
221
|
+
# of integers representing those widths
|
|
222
|
+
#
|
|
223
|
+
# @param data [Array<Array>] The rows and columns of data
|
|
224
|
+
#
|
|
225
|
+
# @param header_row [Array] An optional header row to include in the
|
|
226
|
+
# width calculation.
|
|
227
|
+
#
|
|
228
|
+
# @return [Array<Integer>] the max widths of each column of data.
|
|
229
|
+
#
|
|
230
|
+
############################
|
|
231
|
+
def col_widths(data, header_row = [])
|
|
232
|
+
widths = header_row.map { |c| c.to_s.length }
|
|
233
|
+
data.each do |row|
|
|
234
|
+
row.each_index do |col|
|
|
235
|
+
this_width = row[col].to_s.length
|
|
236
|
+
widths[col] = this_width if this_width > widths[col].to_i
|
|
237
|
+
end # do field
|
|
238
|
+
end # do line
|
|
239
|
+
widths
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Should a given string be displayed via /usr/bin/less?
|
|
243
|
+
# true if stdout is a tty AND the string is > (terminal height - 2)
|
|
244
|
+
# The - 2 accounts for the final newline and an extra line at the
|
|
245
|
+
# bottom of the terminal, for better visual results
|
|
246
|
+
#########################
|
|
247
|
+
def use_less?(text)
|
|
248
|
+
$stdout.tty? && text.lines.size > (terminal_height - 2)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Send a string to the terminal, possibly piping it through 'less'
|
|
252
|
+
# if the number of lines is greater than the number of terminal lines
|
|
253
|
+
#
|
|
254
|
+
# @param text[String] the text to send to the terminal
|
|
255
|
+
#
|
|
256
|
+
# @param show_help[Boolean] should the text have a line at the top
|
|
257
|
+
# showing basic 'less' key commands.
|
|
258
|
+
#
|
|
259
|
+
# @result [void]
|
|
260
|
+
#
|
|
261
|
+
############################
|
|
262
|
+
def show_text(text, show_help = true)
|
|
263
|
+
unless use_less?(text)
|
|
264
|
+
puts text
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
if show_help
|
|
269
|
+
help = "# -- Using /usr/bin/less: ' ' next, 'b' prev, 'q' exit, 'h' help --"
|
|
270
|
+
text = "#{help}\n#{text}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# point stdout through less, print, then restore stdout
|
|
274
|
+
less = IO.popen('/usr/bin/less', 'w')
|
|
275
|
+
|
|
276
|
+
begin
|
|
277
|
+
less.puts text
|
|
278
|
+
|
|
279
|
+
# this catches the quitting of 'less' before all the output
|
|
280
|
+
# is displayed
|
|
281
|
+
rescue Errno::EPIPE
|
|
282
|
+
true
|
|
283
|
+
ensure
|
|
284
|
+
less&.close
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
end # module
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Core
|
|
14
|
+
|
|
15
|
+
# Personal credentials for users of 'xadm', stored in the login keychain
|
|
16
|
+
#
|
|
17
|
+
module SecurityCmd
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
##############################
|
|
21
|
+
##############################
|
|
22
|
+
|
|
23
|
+
# The security command
|
|
24
|
+
SEC_COMMAND = '/usr/bin/security'
|
|
25
|
+
|
|
26
|
+
# exit status when the login keychain can't be accessed because we aren't in a GUI session
|
|
27
|
+
SEC_STATUS_NO_GUI_ERROR = 36
|
|
28
|
+
|
|
29
|
+
# exit status when the keychain password provided is incorrect
|
|
30
|
+
SEC_STATUS_AUTH_ERROR = 51
|
|
31
|
+
|
|
32
|
+
# exit status when the desired item isn't found in the keychain
|
|
33
|
+
SEC_STATUS_NOT_FOUND_ERROR = 44
|
|
34
|
+
|
|
35
|
+
# Module methods
|
|
36
|
+
##############################
|
|
37
|
+
##############################
|
|
38
|
+
|
|
39
|
+
# when this module is included
|
|
40
|
+
def self.included(includer)
|
|
41
|
+
Xolo.verbose_include includer, self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Instance Methods
|
|
45
|
+
##########################
|
|
46
|
+
##########################
|
|
47
|
+
|
|
48
|
+
# Run the security command in interactive mode on a given keychain,
|
|
49
|
+
# passing in a subcommand and its arguments. so that they don't appear in the
|
|
50
|
+
# `ps` output
|
|
51
|
+
#
|
|
52
|
+
# @param cmd [String] the subcommand being passed to 'security' with
|
|
53
|
+
# all needed options. It will not be visible outide this process, so
|
|
54
|
+
# its OK to put passwords into the options.
|
|
55
|
+
#
|
|
56
|
+
# @return [String] the stdout of the 'security' command.
|
|
57
|
+
#
|
|
58
|
+
######
|
|
59
|
+
def run_security(cmd)
|
|
60
|
+
output = Xolo::BLANK
|
|
61
|
+
errs = Xolo::BLANK
|
|
62
|
+
|
|
63
|
+
Open3.popen3("#{SEC_COMMAND} -i") do |stdin, stdout, stderr, wait_thr|
|
|
64
|
+
# pid = wait_thr.pid # pid of the started process.
|
|
65
|
+
stdin.puts cmd
|
|
66
|
+
stdin.close
|
|
67
|
+
|
|
68
|
+
output = stdout.read
|
|
69
|
+
errs = stderr.read
|
|
70
|
+
|
|
71
|
+
@security_exit_status = wait_thr.value # Process::Status object returned.
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# exit 44 is 'The specified item could not be found in the keychain'
|
|
75
|
+
return output.chomp if @security_exit_status.success?
|
|
76
|
+
|
|
77
|
+
case @security_exit_status.exitstatus
|
|
78
|
+
when SEC_STATUS_AUTH_ERROR
|
|
79
|
+
raise Xolo::KeychainError, 'Problem accessing login keychain. Is it locked?'
|
|
80
|
+
|
|
81
|
+
when SEC_STATUS_NOT_FOUND_ERROR
|
|
82
|
+
raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
|
|
83
|
+
|
|
84
|
+
else
|
|
85
|
+
errs.chomp!
|
|
86
|
+
errs =~ /: returned\s+(-?\d+)$/
|
|
87
|
+
errnum = Regexp.last_match(1)
|
|
88
|
+
desc = errnum ? security_error_desc(errnum) : errs
|
|
89
|
+
desc ||= errs
|
|
90
|
+
raise Xolo::KeychainError, "#{desc.gsub("\n", '; ')}; exit status #{@security_exit_status.exitstatus}"
|
|
91
|
+
end # case
|
|
92
|
+
end # run_security
|
|
93
|
+
|
|
94
|
+
# use `security error` to get a description of an error number
|
|
95
|
+
##############
|
|
96
|
+
def security_error_desc(num)
|
|
97
|
+
desc = `#{SEC_COMMAND} error #{num}`
|
|
98
|
+
return if desc.include?('unknown error')
|
|
99
|
+
|
|
100
|
+
desc.chomp.split(num).last
|
|
101
|
+
rescue
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# given a string, wrap it in single quotes and escape internal single quotes
|
|
106
|
+
# and backslashes so it can be used in the interactive 'security' command
|
|
107
|
+
#
|
|
108
|
+
# @param str[String] the string to escape
|
|
109
|
+
#
|
|
110
|
+
# @return [String] the escaped string
|
|
111
|
+
###################
|
|
112
|
+
def security_escape(str)
|
|
113
|
+
# first escape backslashes
|
|
114
|
+
str = str.to_s.gsub '\\', '\\\\\\'
|
|
115
|
+
|
|
116
|
+
# then single quotes
|
|
117
|
+
str.gsub! "'", "\\\\'"
|
|
118
|
+
|
|
119
|
+
# if other things need escaping, add them here
|
|
120
|
+
|
|
121
|
+
"'#{str}'"
|
|
122
|
+
end # security_escape
|
|
123
|
+
|
|
124
|
+
end # module Prefs
|
|
125
|
+
|
|
126
|
+
end # module Admin
|
|
127
|
+
|
|
128
|
+
end # module Xolo
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# main module
|
|
9
|
+
module Xolo
|
|
10
|
+
|
|
11
|
+
module Core
|
|
12
|
+
|
|
13
|
+
module Version
|
|
14
|
+
|
|
15
|
+
VERSION = '2.0.2'.freeze
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end # module
|